This commit is contained in:
ameen soleimani
2023-03-03 19:06:04 -08:00
commit 2e6b647be7
70 changed files with 7424 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
\*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env
.env\*.local
# vercel
.vercel
# typescript
\*.tsbuildinfo
next-env.d.ts

14
.graphclientrc.yml Normal file
View File

@@ -0,0 +1,14 @@
sources:
# - name: goerli
# handler:
# graphql:
# endpoint: https://api.studio.thegraph.com/query/40189/privacy-pools/v0.0.6
- name: privacy-pools
handler:
graphql:
endpoint: https://api.thegraph.com/subgraphs/name/ameensol/privacy-pools
documents:
- ./src/query/commitments.graphql
- ./src/query/subsetRootsByTimestamp.graphql
- ./src/query/subsetDataByNullifier.graphql

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
strict-peer-dependencies = false
legacy-peer-deps = true

4
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,4 @@
tabWidth: 2
semi: true
trailingComma: none
singleQuote: true

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
State Management
jotai atom:
- has default value matching shape of typed object
hooks:
- check for default value, then
- populate state via queries or calculations
- returns direct values
components:
- retrieving state from hook now initializes it
- can still modify state using the jotai atom
This is a [wagmi](https://wagmi.sh) + [RainbowKit](https://rainbowkit.com) + [Next.js](https://nextjs.org) project bootstrapped with [`create-wagmi`](https://github.com/wagmi-dev/wagmi/tree/main/packages/create-wagmi)
# Getting Started
Run `npm run dev` in your terminal, and then open [localhost:3000](http://localhost:3000) in your browser.
Once the webpage has loaded, changes made to files inside the `src/` directory (e.g. `src/pages/index.tsx`) will automatically update the webpage.
# Learn more
To learn more about [Next.js](https://nextjs.org) or [wagmi](https://wagmi.sh), check out the following resources:
- [wagmi Documentation](https://wagmi.sh) learn about wagmi Hooks and API.
- [wagmi Examples](https://wagmi.sh/examples/connect-wallet) a suite of simple examples using wagmi.
- [RainbowKit Documentation](https://rainbowkit.com/docs/introduction) learn more about RainbowKit (configuration, theming, advanced usage, etc).
- [Next.js Documentation](https://nextjs.org/docs) learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

3
index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module "zxcvbn"
declare module "privacy-pools"
declare module "snarkjs"

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "my-app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prettier-format": "prettier --config .prettierrc.yaml 'src/**/*.ts' --write",
"prettier-format-tsx": "prettier --config .prettierrc.yaml 'src/**/*.tsx' --write"
},
"browser": {
"fs": false
},
"dependencies": {
"@chakra-ui/icons": "^2.0.14",
"@chakra-ui/react": "^2.4.9",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@rainbow-me/rainbowkit": "^0.8.1",
"@types/next": "^9.0.0",
"axios": "^1.2.2",
"constants": "^0.0.2",
"ethers": "^5.7.2",
"fastfile": "^0.0.20",
"framer-motion": "^6.5.1",
"fs": "^0.0.1-security",
"graphql": "^16.6.0",
"jotai": "^1.12.0",
"lodash": "^4.17.21",
"next": "^13.0.0",
"pools-ts": "github:ameensol/pools-ts",
"privacy-pools": "github:ameensol/privacy-pools",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.7.1",
"readline": "^1.3.0",
"snarkjs": "^0.5.0",
"urql": "^3.0.3",
"wagmi": "^0.9.6",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@types/node": "^18.13.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@wagmi/core": "^0.9.5",
"eslint": "^8.15.0",
"eslint-config-next": "^12.0.4",
"http-server": "^14.1.1",
"prettier": "^2.8.4",
"ts-loader": "^9.4.2",
"typescript": "^4.7.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,59 @@
import {
Container,
Button,
HStack,
Menu,
MenuButton,
MenuList,
MenuItem,
Stack,
Select
} from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { useAtom } from 'jotai';
import { useOptions } from '../hooks';
import { assetAtom, denominationAtom } from '../state';
export function AssetDenominationBar() {
const [asset, setAsset] = useAtom(assetAtom);
const [denomination, setDenomination] = useAtom(denominationAtom);
const { assetOptions, denominationOptions } = useOptions();
return (
<Stack direction={['row']}>
<Menu>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
{denomination}
</MenuButton>
<MenuList>
{denominationOptions.map((_denomination, i) => (
<MenuItem
value={_denomination}
key={`${_denomination}-${i}-option`}
onClick={() => setDenomination(_denomination.toString())}
>
{_denomination}
</MenuItem>
))}
</MenuList>
</Menu>
<Menu>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
{asset}
</MenuButton>
<MenuList>
{assetOptions.map((_asset, i) => (
<MenuItem
value={_asset}
key={`${_asset}-${i}-option`}
onClick={() => setAsset(_asset.toString())}
>
{_asset}
</MenuItem>
))}
</MenuList>
</Menu>
</Stack>
);
}

View File

@@ -0,0 +1,32 @@
import { Flex, BoxProps } from '@chakra-ui/react';
interface CenteredPageProps extends BoxProps {
children?: React.ReactNode;
}
export const CenteredPage: React.FC<CenteredPageProps> = ({
children,
...boxProps
}) => {
return (
<>
<Flex
position="relative"
wrap="wrap"
top={0}
bottom={0}
w="100vw"
// h={['calc(100vh - 308px)', 'calc(100vh - 160px)', 'calc(90vh - 112px)']}
mt={[4, 0]}
h={['auto', 'calc(100vh - 160px)', 'calc(90vh - 112px)']}
align="center"
justify="center"
{...boxProps}
>
{children}
</Flex>
</>
);
};
export default CenteredPage;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import {
Box,
BoxProps,
Flex,
Link,
Text,
Icon,
Divider
} from '@chakra-ui/react';
import { FaTwitter, FaGithub } from 'react-icons/fa';
import { AiOutlineCopyright } from 'react-icons/ai';
export function DappFooter() {
return (
<Box
as="footer"
position="fixed"
bottom="0"
py={2}
zIndex="10"
mt={20}
textAlign="center"
w="100%"
bg="rgb(23,25,35)"
bgGradient="linear-gradient(0deg, rgba(23,25,35,1) 54%, rgba(45,55,72,1) 97%)"
color="gray.400"
>
<Flex direction="column" alignItems="center">
<Divider
orientation="horizontal"
h="1px"
w="60%"
bg="gray.500"
my={2}
/>
<Flex alignItems="center">
<AiOutlineCopyright />
<Text fontSize="sm" mr={1}>
Privacy Pools,
</Text>
<Text fontSize="sm">2023</Text>
</Flex>
</Flex>
</Box>
);
}
export default DappFooter;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import {
Box,
BoxProps,
Flex,
Link,
Text,
Icon,
Divider
} from '@chakra-ui/react';
import HeaderNavbar from './HeaderNavbar';
import DappFooter from './DappFooter';
interface LayoutProps extends BoxProps {
children?: React.ReactNode;
title?: string;
}
export const DappLayout: React.FC<LayoutProps> = ({
title,
children,
...boxProps
}) => {
return (
<Box
minH="100vh"
minW="100vw"
w="100vw"
h="100%"
pb={0}
mt="0"
backdropBlur="80px"
overflow="scroll"
// bg="gray.100"
// bgGradient="linear-gradient(45deg, #b8faf4 0%, #a8bdfc 25%, #c5b0f6 50%, #ffe5f9 75%, #f0ccc3 100%)"
bgGradient="linear-gradient(45deg, #b8faf480 0%, #a8bdfc80 25%, #c5b0f680 50%, #ffe5f980 75%, #f0ccc380 100%)"
>
<HeaderNavbar title={title} />
<Box {...boxProps} mb={10}>
{children}
</Box>
<Flex mt={10}>
<DappFooter />
</Flex>
</Box>
);
};
export default DappLayout;

View File

@@ -0,0 +1,167 @@
import {
Box,
BoxProps,
Center,
Container,
Flex,
Link,
Stack
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import { HamburgerIcon } from '@chakra-ui/icons';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { Heading } from '@chakra-ui/react';
import Head from 'next/head';
import NextLink from 'next/link';
import React from 'react';
import { FaSwimmingPool } from 'react-icons/fa';
import { NoteWalletConnectButton } from './NoteWallet';
const growShrinkProps = {
_hover: {
transform: 'scale(1.025)'
},
_active: {
transform: 'scale(0.95)'
},
transition: '0.125s ease'
};
interface HeaderNavbarProps extends BoxProps {
title?: string;
}
export const HeaderNavbar: React.FC<HeaderNavbarProps> = ({ title }) => {
const router = useRouter();
const [activeLink, setActiveLink] = useState(router.pathname);
const isActiveLink = (href: string) => {
return router.pathname === href;
};
const handleLinkClick = (path: string) => {
router.push(path);
setActiveLink(path);
};
const linkStyle = (link: string) => ({
padding: isActiveLink(link) ? '17px 30px' : '',
borderRadius: '18px',
background: isActiveLink(link) ? 'white' : 'none',
color: isActiveLink(link) ? 'black' : 'inherit'
// border: isActiveLink(link) ?"4px solid black":"",
});
return (
<>
<Head>
<title>{title}</title>
</Head>
<Container w="100vw" minW="100vw">
<Flex
direction={['column', 'column', 'row']}
justify="space-between"
align="center"
py={4}
gap={4}
>
<Link
as={NextLink}
href="/"
{...growShrinkProps}
mb={{ base: 5, md: 0 }}
>
<Flex align="center">
<Box as={FaSwimmingPool} boxSize={8} mr={4} />
<Box as="h1" fontSize={['xl', '2xl']} fontWeight="bold" flex="1">
PRIVACY POOLS
</Box>
</Flex>
</Link>
<Flex gap={{ base: 2, md: 4 }}>
<NoteWalletConnectButton />
<ConnectButton
accountStatus="address"
chainStatus="icon"
showBalance={{
smallScreen: true,
largeScreen: true
}}
/>
</Flex>
</Flex>
<Flex justifyContent="center">
<Container mt={5} px={0}>
<Flex
boxShadow="dark-lg"
backdropBlur="8px"
bg="rgb(23,25,35)"
bgGradient="linear-gradient(0deg, rgba(23,25,35,1) 54%, rgba(45,55,72,1) 97%)"
borderRadius="10px"
fontWeight="bold"
justifyContent="center"
alignItems="center"
overflow="visible"
color="white"
>
<Stack
width="100%"
justify={['space-evenly']}
alignItems="center"
direction="row"
px={0}
py={2}
gap={[0, 3, 10]}
maxH="50px"
>
<Link
as={NextLink}
fontSize={['xs', 'md']}
href="/stats"
style={linkStyle('/stats')}
{...growShrinkProps}
>
Stats
</Link>
<Link
as={NextLink}
fontSize={['xs', 'md']}
href="/deposit"
style={linkStyle('/deposit')}
{...growShrinkProps}
>
Deposit
</Link>
<Link
as={NextLink}
fontSize={['xs', 'md']}
href="/withdraw"
style={linkStyle('/withdraw')}
{...growShrinkProps}
>
Withdraw
</Link>
<Link
as={NextLink}
fontSize={['xs', 'md']}
href="/explorer"
style={linkStyle('/explorer')}
{...growShrinkProps}
>
Explorer
</Link>
</Stack>
</Flex>
</Container>
</Flex>
</Container>
</>
);
};
export default HeaderNavbar;

View File

@@ -0,0 +1,85 @@
import React from 'react';
import {
Box,
BoxProps,
Container,
Link,
Heading,
Flex,
Stack
} from '@chakra-ui/react';
import Head from 'next/head';
import NextLink from 'next/link';
const growShrinkProps = {
_hover: {
transform: 'scale(1.025)'
},
_active: {
transform: 'scale(0.95)'
},
transition: '0.125s ease'
};
interface LayoutProps extends BoxProps {
children?: React.ReactNode;
title?: string;
}
export const HomeLayout: React.FC<LayoutProps> = ({
title,
children,
...boxProps
}) => {
return (
<>
<Head>
<title>{title}</title>
</Head>
<Container maxW="98vw" minW="216px">
<Flex
direction={['column', 'column', 'row']}
justify="space-between"
align="center"
m={4}
gap={4}
>
<Heading size="lg">🌊 Privacy Pools</Heading>
<Container w="fit-content">
<Stack
color="blue.800"
fontWeight="bold"
justifyContent="center"
alignItems="center"
direction={['column', 'row']}
px={8}
py={2}
gap={[0, 8]}
>
<Link as={NextLink} href="/stats" {...growShrinkProps}>
Stats
</Link>
<Link as={NextLink} href="/deposit" {...growShrinkProps}>
Deposit
</Link>
<Link as={NextLink} href="/withdraw" {...growShrinkProps}>
Withdraw
</Link>
<Link as={NextLink} href="/explorer" {...growShrinkProps}>
Explorer
</Link>
</Stack>
</Container>
</Flex>
</Container>
<Box {...boxProps}>{children}</Box>
</>
);
};
export default HomeLayout;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,155 @@
import {
Box,
Heading,
Text,
Tag,
Flex,
Link,
Avatar,
Icon,
TagLabel,
Divider,
useClipboard,
Tooltip
} from '@chakra-ui/react';
import { useState } from 'react';
import { AiOutlineCopyright } from 'react-icons/ai';
import { FiExternalLink } from 'react-icons/fi';
import { FaTwitter, FaGithub } from 'react-icons/fa';
interface ClickToCopyAddressProps {
tagImage: string;
tagLabel: string;
onCopyCall: () => void;
}
const ClickToCopyAddress: React.FC<ClickToCopyAddressProps> = ({
tagImage,
tagLabel,
onCopyCall
}) => {
const [tooltipText, setTooltipText] = useState('Click to copy address');
const handleCopyAddress = () => {
onCopyCall();
setTooltipText(`Copied ${tagLabel} to clipboard!`);
setTimeout(() => {
setTooltipText('Click to copy address');
}, 2000);
};
return (
<Tooltip label={tooltipText}>
<Tag
size={{ base: 'sm', md: 'md' }}
bg="gray.700"
color="gray.200"
borderRadius="full"
py={1}
mx={1}
onClick={handleCopyAddress}
_hover={{ cursor: 'pointer' }}
>
<Avatar src={tagImage} size="xs" name="dnsNameTwo.eth" ml={-1} mr={2} />
<TagLabel>{tagLabel}</TagLabel>
</Tag>
</Tooltip>
);
};
const Footer: React.FC = () => {
const devAddress1 = 'ameensol.eth';
const { copied: copied1, onCopy: onCopyAdd1 }: any =
useClipboard(devAddress1);
return (
<Box>
<Box
pt="5rem"
display="flex"
flexDirection="column"
textAlign="center"
justifyContent="center"
alignItems="center"
bg="rgb(23,25,35)"
bgGradient="linear-gradient(0deg, rgba(23,25,35,1) 54%, rgba(45,55,72,1) 97%)"
color="white"
>
<Box width={{ base: '80%', md: '40%' }} mb={20}>
<Box
bg="black"
color="gray.200"
border="3px solid gray"
borderRadius={20}
px={8}
py={10}
>
<Text textAlign="left">
&quot;The best known cryptographic problem is that of privacy:
preventing the unauthorized extraction of information from
communications over an insecure channel.&quot;
</Text>
<Text fontWeight="bold" textAlign="right" mt={4}>
- Diffie and Hellman, &quot;New Directions in Cryptography&quot;
1976
</Text>
</Box>
</Box>
<Flex
mt={10}
alignItems="center"
fontSize={{ base: 'md', md: 'xl' }}
justifyContent="center"
color="gray.400"
px={2}
>
This work is made possible by
<Link href="https://github.com" isExternal ml={2} color="white">
MolochDAO
<Icon as={FiExternalLink} boxSize={4} ml={1} />
</Link>
</Flex>
<Heading as="h1" size="2xl" mb="1rem" mt={2} px={5}>
Built from the ground up. Built on Ethereum. Deployed on Optimism.
</Heading>
</Box>
<Box
as="footer"
mt="auto"
pb={10}
textAlign="center"
w="100%"
bg="gray.900"
color="gray.400"
>
<Flex direction="column" alignItems="center">
<Divider
orientation="horizontal"
h="1px"
w="60%"
bg="gray.500"
my={5}
/>
<Flex alignItems="center">
<AiOutlineCopyright />
<Text fontSize="lg" ml={1}>
Privacy Pools,
</Text>
<Text fontSize="lg">2023</Text>
</Flex>
<Flex alignItems="center" mt={2}>
<Text fontSize="xs" color="gray.500" mr={2}>
ALL RIGHTS RESERVED
</Text>
</Flex>
</Flex>
</Box>
</Box>
);
};
export default Footer;

View File

@@ -0,0 +1,173 @@
import {
Box,
Flex,
Image,
Text,
Heading,
Link,
Button
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { AiOutlineDown } from 'react-icons/ai';
const variants = {
hidden: {
opacity: 1,
y: 15
},
visible: {
opacity: 1,
y: 0
}
};
const HeroSection: React.FC = () => {
return (
<Box>
<Box h="100vh" display="flex" alignItems="center">
<Box>
<motion.div
variants={variants}
initial="hidden"
animate="visible"
transition={{ duration: 0.5 }}
>
<Flex direction="column" alignItems="center">
<Flex direction={{ base: 'column', lg: 'row' }} alignItems="top">
<Box mt={{ base: 20, md: 2 }}>
<Text
fontSize={{ base: '2xl', md: '5xl' }}
fontWeight="bold"
mb={4}
pr={{ base: 0, lg: 200 }}
textAlign={{
base: 'center',
lg: 'left'
}}
>
Discover a New Privacy on Ethereum
</Text>
<Text
fontSize={{ base: 'md', md: 'lg' }}
lineHeight="tall"
mr={{ base: 0, lg: 40 }}
textAlign={{
base: 'center',
lg: 'left'
}}
>
Privacy Pools allow you to generate a brand new Ethereum
address that is completely unlinkable to any prior
transaction history. But our privacy-preserving technology
does much more than that.
</Text>
<Link
target="_blank"
href="/stats"
textDecoration="none"
_hover={{ textDecoration: 'none' }}
>
<Button
mt={10}
px={4}
py={2}
bg="black"
color="white"
rounded="full"
border="4px solid black"
_hover={{
bg: 'gray.100',
color: 'black',
borderColor: 'black',
transform: 'scaleX(1.05)',
textDecoration: 'none'
}}
transition="all 0.2s"
fontWeight="semibold"
boxShadow="2xl"
display={{
base: 'none',
md: 'block'
}}
>
GET PRIVACY NOW
</Button>
</Link>
</Box>
<Box
w={{ base: '100%', lg: '50%' }}
textAlign={{ base: 'center', lg: 'right' }}
mt={{ base: 8, md: 0 }}
mr={50}
>
<motion.div
variants={variants}
initial="hidden"
animate="visible"
transition={{
duration: 0.95,
delay: 0.95,
repeat: Infinity,
repeatType: 'reverse'
}}
>
<Image
boxSize={{
base: '350px',
md: '450px'
}}
src="eth_pools_logo_bw.svg"
alt="Image"
/>
</motion.div>
</Box>
</Flex>
<Box
position="relative"
bottom="-40px"
textAlign="center"
alignSelf="center"
mt={{ base: 20, md: 40 }}
>
{/* <motion.div
variants={variants}
initial="hidden"
animate="visible"
transition={{
duration: 0.95,
delay: 0.95,
repeat: Infinity,
repeatType: "reverse",
}}
> */}
<Text
fontSize={{ base: 'md', md: 'lg' }}
fontWeight="bold"
mb={2}
>
Scroll now to learn how Privacy Pools protect you from bad
depositors.
</Text>
<Button
size="lg"
disabled={true}
sx={{
_hover: 'none',
_active: 'none',
cursor: 'pointer'
}}
bg="none"
>
<AiOutlineDown />
</Button>
{/* </motion.div> */}
</Box>
</Flex>
</motion.div>
</Box>
</Box>
</Box>
);
};
export default HeroSection;

View File

@@ -0,0 +1,213 @@
import { useState, ReactElement, JSXElementConstructor } from 'react';
import {
Box,
Flex,
Text,
IconButton,
Collapse,
Button,
Center,
Link,
useBreakpointValue
} from '@chakra-ui/react';
import { FaInfoCircle } from 'react-icons/fa';
import { VscDebugStart } from 'react-icons/vsc';
import { HiOutlineBeaker } from 'react-icons/hi';
import { FiSettings } from 'react-icons/fi';
import { AiOutlineLock, AiOutlineDown, AiOutlineUp } from 'react-icons/ai';
interface ListItemProps {
icon: ReactElement<any, string | JSXElementConstructor<any>> | undefined;
text: string;
paragraph: string;
}
function ListItem({ icon, text, paragraph }: ListItemProps) {
const [isOpen, setIsOpen] = useState(true);
const handleOpen = () => {
setIsOpen(!isOpen);
};
return (
<Flex align="center">
<Box display={{ base: 'none', md: 'block' }}>
<IconButton
icon={icon}
aria-label="info"
size="md"
onClick={handleOpen}
mx={5}
borderRadius="50%"
color="white"
bg="black"
_hover={{
bg: 'gray.200',
color: 'black',
border: '4px solid black'
}}
/>
</Box>
<Flex
direction="column"
// onMouseEnter={handleOpen}
// onMouseLeave={handleOpen}
p={4}
borderRadius={8}
border="4px solid"
bg="gray.100"
boxShadow="lg"
my={2}
w="60vw"
_hover={{
cursor: 'pointer',
bg: 'white',
color: 'black'
}}
>
<Flex alignItems="center" justifyContent="space-between">
<Text ml={2} fontWeight="bold">
{text}
</Text>
{isOpen ? (
<AiOutlineUp size="20px" fontWeight="bold" />
) : (
<AiOutlineDown size="20px" fontWeight="bold" />
)}
</Flex>
<Collapse in={isOpen} animateOpacity>
<Box my={4} p={4} rounded="md">
<Text fontSize="sm" lineHeight="tall">
{paragraph}
</Text>
</Box>
</Collapse>
</Flex>
</Flex>
);
}
const InfoSection = () => {
const btnPadding = useBreakpointValue({ base: '20px', lg: '30px' });
const btnFontSize = useBreakpointValue({ base: 'xs', sm: 'md', lg: 'lg' });
return (
<Box pb={20}>
<Center>
<Flex
direction="column"
alignItems="center"
w={{ base: '90%', md: '70%' }}
my={30}
>
<Text fontSize={{ base: 'md', md: 'xl' }} mt={10} textAlign="center">
Introducing a demonstration in
</Text>
<Text
fontSize={{ base: '2xl', md: '5xl' }}
fontWeight="bold"
textAlign="center"
>
SELF.SOLVEREIGN.ANONYMITY
</Text>
<Text fontSize={{ base: 'md', md: 'xl' }} mb={20} textAlign="center">
A no compromise solution to credibly neutral privacy for Ethereum
</Text>
<ListItem
icon={<AiOutlineLock />}
text="Protect Your Privacy"
paragraph="Privacy Pools are designed to break the link between your original deposit address and your new withdrawal address. By depositing your funds into a common pool and leaving a cryptographic commitment to a secret value, you can withdraw your funds using a zero-knowledge proof that ensures your new withdrawal address is entirely new and unlinkable. With Privacy Pools, your transaction history remains private and untraceable."
/>
<ListItem
icon={<FiSettings />}
text="Customize Your Privacy Sets"
paragraph="The privacy protocol allows for configurable privacy sets that enable honest users to exclude hackers and bad actors from their transactions. This makes it difficult for money launderers to take advantage of the system. You can choose to exclude anyone you don't trust from your privacy sets, ensuring that your transactions are secure and private."
/>
<ListItem
icon={<HiOutlineBeaker />}
text="Conduct Open Source Research"
paragraph="Our decentralized privacy application is an open source research project that is dedicated to advancing the cause of privacy on the Ethereum blockchain. Our technology is open source and transparent, so you can be confident that your privacy is being protected by a community of experts and enthusiasts."
/>
<ListItem
icon={<VscDebugStart />}
text="Get Started Today"
paragraph="If you're looking for a privacy-preserving wallet that is both secure and easy to use, look no further than our decentralized privacy application. Try it today and experience the freedom and security of truly private transactions on the Ethereum blockchain."
/>
</Flex>
</Center>
<Flex w="100%" justifyContent="center">
<Flex
mt={5}
justifyContent="center"
alignItems="center"
direction={{ base: 'column', md: 'row' }}
>
<Link
target="_blank"
href="https://github.com/privacy-pools/the-lounge"
mr={{ base: 0, md: 10 }}
textDecoration="none"
_hover={{ textDecoration: 'none' }}
>
<Button
mb={10}
px={btnPadding}
py={2}
bg="gray.100"
color="black"
rounded="full"
border="4px solid"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.05)'
}}
transition="all 0.2s"
fontWeight="semibold"
boxShadow="xl"
fontSize={btnFontSize}
w="100%"
>
GO TO DOCS
</Button>
</Link>
<Link
target="_blank"
href="/stats"
textDecoration="none"
_hover={{ textDecoration: 'none' }}
>
<Button
px={btnPadding}
py={2}
mb={10}
bg="black"
color="white"
rounded="full"
border="4px solid black"
_hover={{
bg: 'white',
color: 'black',
borderColor: 'black',
transform: 'scaleX(1.05)'
}}
transition="all 0.2s"
fontWeight="semibold"
boxShadow="xl"
fontSize={btnFontSize}
w="100%"
>
GET PRIVACY NOW
</Button>
</Link>
<Text display={{ base: 'block', md: 'none' }}>
Open on Desktop for better experience.
</Text>
</Flex>
</Flex>
</Box>
);
};
export default InfoSection;

View File

@@ -0,0 +1,136 @@
import { Flex, Box, Link, IconButton, useDisclosure } from '@chakra-ui/react';
import NextLink from 'next/link';
import { FaSwimmingPool } from 'react-icons/fa';
import { HamburgerIcon } from '@chakra-ui/icons';
import { useState, useEffect } from 'react';
const NavBar = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 0) {
setScrolled(true);
} else {
setScrolled(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<Flex
alignItems="center"
justifyContent="space-between"
position="sticky"
top={0}
zIndex={10}
py={5}
backdropFilter="auto"
backdropBlur="8px"
filter="auto"
>
<Link as={NextLink} href="/" _hover={{ textDecoration: 'none' }}>
<Flex align="center">
<Box as={FaSwimmingPool} boxSize={8} mr={4} />
<Box as="h1" fontSize="2xl" fontWeight="bold" flex="1">
PRIVACY POOLS
</Box>
</Flex>
</Link>
<Box display={{ base: 'none', md: 'block' }}>
<Link
fontWeight="bold"
mr={4}
target="_blank"
href="https://github.com/privacy-pools/the-lounge"
>
docs
</Link>
{!scrolled && (
<Link
fontWeight="bold"
mr={4}
target="_blank"
href="https://github.com/privacy-pools"
>
github
</Link>
)}
<Link fontWeight="bold" mr={4} target="_blank" href="/explorer">
explorer
</Link>
<Link fontWeight="bold" mr={4} target="_blank" href="/stats">
dapp
</Link>
</Box>
<IconButton
aria-label="Navigation menu"
icon={<HamburgerIcon />}
size="xl"
fontWeight="bold"
variant="ghost"
p={3}
borderRadius="full"
display={{ base: 'block', md: 'none' }}
onClick={isOpen ? onClose : onOpen}
/>
{isOpen && (
<Box
bg="white"
position="absolute"
top="60px"
right="0"
py={2}
px={4}
display={{ base: 'block', md: 'none' }}
>
<Link
mr={4}
display="block"
mb={2}
fontWeight="bold"
target="_blank"
href="https://github.com/privacy-pools/the-lounge"
>
docs
</Link>
<Link
mr={4}
display="block"
mb={2}
fontWeight="bold"
target="_blank"
href="https://github.com/privacy-pools"
>
github
</Link>
<Link
mr={4}
display="block"
mb={2}
fontWeight="bold"
target="_blank"
href="/explorer"
>
explorer
</Link>
<Link
mr={4}
display="block"
mb={2}
fontWeight="bold"
target="_blank"
href="/stats"
>
dapp
</Link>
</Box>
)}
</Flex>
);
};
export default NavBar;

View File

@@ -0,0 +1,88 @@
import { useState } from 'react';
import { Box, Button, Flex, Text, Tooltip } from '@chakra-ui/react';
import { useAtom } from 'jotai';
import { stageAtom } from '../../state';
import { AiOutlineInfoCircle } from 'react-icons/ai';
export default function Connect() {
const [_stage, setStage] = useAtom(stageAtom);
return (
<Box>
<Flex my={20} mx={4} direction="column">
<Button
onClick={() => setStage('Unlock')}
// mx={2}
my={2}
px={{ base: '20px', lg: '30px' }}
py={{ base: '10px', lg: '20px' }}
bg="white"
color="black"
rounded="full"
border="4px solid"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.01)'
}}
transition="all 0.2s"
fontWeight="semibold"
boxShadow="xl"
fontSize={{ base: 'xs', sm: 'sm', lg: 'md' }}
w="100%"
>
{' '}
Already Have A Note Wallet
</Button>
<Button
onClick={() => setStage('Create')}
// mx={2}
my={2}
px={{ base: '20px', lg: '30px' }}
py={{ base: '10px', lg: '20px' }}
bg="black"
color="white"
rounded="full"
border="4px solid black"
_hover={{
bg: 'white',
color: 'black',
borderColor: 'black',
transform: 'scaleX(1.01)'
}}
transition="all 0.2s"
fontWeight="semibold"
boxShadow="xl"
fontSize={{ base: 'xs', sm: 'sm', lg: 'md' }}
w="100%"
>
{' '}
Create New Note Wallet
</Button>
</Flex>
<Box
textAlign="center"
fontSize="xs"
_hover={{
cursor: 'pointer',
fontWeight: 'bold'
}}
>
<Tooltip
label="TODO: @justin to put some info about note wallet"
bg="gray.800"
color="white"
fontSize="sm"
borderRadius="md"
>
<Flex alignItems="center" justifyContent="center">
<AiOutlineInfoCircle />
<Text ml={1}>What is Note Wallet?</Text>
</Flex>
</Tooltip>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,437 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Link,
Container,
FormControl,
FormLabel,
FormErrorMessage,
Input,
VStack,
Text,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Flex,
Textarea,
Spinner,
Icon
} from '@chakra-ui/react';
import {
MdOutlineArrowBackIos,
MdOutlineArrowForwardIos
} from 'react-icons/md';
import { FiExternalLink } from 'react-icons/fi';
import { FiDownload } from 'react-icons/fi';
import PasswordInput from './PasswordInput';
import { useAtom } from 'jotai';
import {
stageAtom,
EncryptedJson,
encryptedJsonAtom,
downloadUrlAtom
} from '../../state';
import * as zxcvbn from 'zxcvbn';
import * as ethers from 'ethers';
import { NoteWalletV2 } from 'privacy-pools';
const MIN_PW_LENGTH = 8;
const MIN_PW_STRENGTH = 3;
export default function Create() {
const [mnemonic, setMnemonic] = useState('');
const [mnemonicError, setMnemonicError] = useState('');
const [tempWallet, setTempWallet] = useState<ethers.Wallet>();
const [tempEncryptedMnemonic, setTempEncryptedMnemonic] =
useState<EncryptedJson>();
const [isErrored, setIsErrored] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isEncrypting, setIsEncrypting] = useState(false);
const [encryptProgress, setEncryptProgress] = useState(0);
const [_stage, setStage] = useAtom(stageAtom);
const [_encryptedJson, setEncryptedJson] = useAtom(encryptedJsonAtom);
const [downloadUrl, setDownloadUrl] = useAtom(downloadUrlAtom);
useEffect(() => {
if (tempWallet?.mnemonic?.phrase) {
setMnemonic(tempWallet.mnemonic.phrase);
}
}, [tempWallet]);
const [currentTab, setCurrentTab] = useState(0);
const handleTabChange = (index: number) => {
setCurrentTab(index);
};
const [tabIndex, setTabIndex] = useState(0);
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTabIndex(parseInt(event.target.value, 10));
};
const download = () => {
console.log('inside download');
if (typeof tempEncryptedMnemonic === 'undefined') return;
const blob = new Blob([JSON.stringify(tempEncryptedMnemonic, null, 2)], {
type: 'text/plain'
});
const url = URL.createObjectURL(blob);
setDownloadUrl(url);
const element = document.createElement('a');
element.setAttribute('href', url);
element.setAttribute('target', '_blank');
element.setAttribute(
'download',
`privacy-pools-encrypted-json-0x${tempEncryptedMnemonic?.address}.json`
);
element.click();
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
if (typeof tempWallet === 'undefined') return;
const formData = new FormData(event.target as HTMLFormElement);
const [_password, _confirm] = [...formData.values()];
if (_password !== _confirm) {
setIsErrored(true);
setErrorMessage('Passwords do not match');
return;
} else if (_password.toString().length < MIN_PW_LENGTH) {
setIsErrored(true);
setErrorMessage(
`Password must be at least ${MIN_PW_LENGTH} characters long`
);
return;
} else {
setIsErrored(false);
setErrorMessage('');
}
try {
const result = zxcvbn(_password);
const { score, feedback } = result;
if (score < MIN_PW_STRENGTH) {
setIsErrored(true);
if (feedback.warning) {
setErrorMessage(feedback.warning);
} else {
setErrorMessage(`Password is too weak.`);
}
return;
} else {
const _noteWallet = new NoteWalletV2(tempWallet.mnemonic.phrase, 0);
setIsEncrypting(true);
setEncryptProgress;
_noteWallet
.encryptToJson(_password)
.then((encryptedJson: string) => {
const parsedJson = JSON.parse(encryptedJson);
const blob = new Blob([JSON.stringify(parsedJson, null, 2)]);
const url = URL.createObjectURL(blob);
setDownloadUrl(url);
setTempEncryptedMnemonic(parsedJson);
setIsEncrypting(false);
})
.then(() => setTimeout(() => setIsEncrypting(false), 1))
.then(() => setCurrentTab(2));
}
} catch (err) {
console.error(err);
setIsErrored(true);
setIsEncrypting(false);
}
};
return (
<Container width="100%">
<Flex width="100%" justifyContent="center">
<Tabs
index={currentTab}
onChange={handleTabChange}
variant="enclosed"
width="100%"
isFitted
>
<TabList mb="1em" height="50px">
<Tab _selected={{ color: 'white', bg: 'gray.900' }}>
<Text fontSize="sm">1 Create Mnemonic</Text>
</Tab>
{tempWallet ? (
<Tab _selected={{ color: 'white', bg: 'gray.900' }}>
<Text fontSize="sm">2 Create Password</Text>
</Tab>
) : (
<Tab _selected={{ color: 'white', bg: 'gray.900' }} isDisabled>
<Text fontSize="sm">2 Create Password</Text>
</Tab>
)}
{tempEncryptedMnemonic ? (
<Tab _selected={{ color: 'white', bg: 'gray.900' }}>
<Text fontSize="sm">3 Download Secrets</Text>
</Tab>
) : (
<Tab _selected={{ color: 'white', bg: 'gray.900' }} isDisabled>
<Text fontSize="sm">3 Download Secrets</Text>
</Tab>
)}
</TabList>
<TabPanels>
<TabPanel minHeight="400px">
<Box>
<form>
<VStack spacing={4} align="stretch">
<FormControl isInvalid={!!mnemonicError}>
<FormLabel fontSize="md">
Create Your 12 Words Mnemonic
</FormLabel>
<Textarea
value={mnemonic || ''}
onChange={(e) => setMnemonic(e.target.value)}
placeholder="Enter your random Mnemonic"
focusBorderColor="gray.400"
bg="gray.50"
size={{ base: 'xs', md: 'md' }}
fontFamily="monospace"
fontWeight="normal"
/>
<FormErrorMessage>{mnemonicError}</FormErrorMessage>
</FormControl>
<Flex
justifyContent="center"
fontSize={{ base: 'xs', md: 'xs' }}
direction={{
base: 'column',
md: 'row'
}}
alignItems="center"
>
<Text color="gray.700">
Mnemonic Codes can be safely generated using many
</Text>
<Link
color="blue.500"
target="_blank"
href="https://iancoleman.io/bip39/"
ml={1}
>
Open source tools
<Icon as={FiExternalLink} boxSize={3} ml={1} />
</Link>
</Flex>
<Flex alignItems="center" justifyContent="center">
<Text fontWeight="bold">OR</Text>
</Flex>
<Button
onClick={(e) => {
e.preventDefault();
setTempWallet(
ethers.Wallet.createRandom({
path: `m/44'/9777'/0'/0/0`
})
);
}}
size="md"
w="full"
type="submit"
bg="gray.200"
color="black"
boxShadow="2xl"
borderColor="red"
_hover={{
bg: 'gray.700',
color: 'white',
borderColor: 'gray.600',
transform: 'scaleX(1.01)'
}}
>
Generate A Random Mnemonic
</Button>
<Button
size="lg"
bg="black"
color="white"
w="100%"
mx={2}
my={8}
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.01)'
}}
onClick={() => {
if (mnemonic.trim().split(' ').length !== 12) {
setMnemonicError(
'Mnemonic should be exactly 12 words separated by space'
);
} else {
setMnemonicError('');
setCurrentTab(1);
}
}}
>
<Flex alignItems="center">
<Text mr={2}>GO TO NEXT STEP</Text>
<MdOutlineArrowForwardIos size={15} />
</Flex>
</Button>
<Flex justifyContent="center" fontSize="sm" mt={8}>
<Link
color="blue.500"
onClick={() => setStage('Connect')}
ml={1}
>
<Flex alignItems="center">
<MdOutlineArrowBackIos size={15} />
<Text ml={1}>Go Back</Text>
</Flex>
</Link>
</Flex>
</VStack>
</form>
</Box>
</TabPanel>
<TabPanel minHeight="400px">
<Box>
<form onSubmit={handleSubmit}>
<Box w="full">
<FormControl>
<FormLabel>Wallet Identifier</FormLabel>
<Input
readOnly
size="lg"
bg="gray.50"
focusBorderColor="gray.400"
type="email"
fontSize="sm"
fontFamily="monospace"
cursor="not-allowed"
value={tempWallet?.address.slice(2)}
isDisabled={isEncrypting}
/>
</FormControl>
<FormLabel fontSize="md" mt={5}>
Create A Strong Password
</FormLabel>
<FormControl isInvalid={isErrored}>
<PasswordInput
placeholder="Enter password"
name="password"
isInvalid={isErrored}
isDisabled={isEncrypting}
/>
<PasswordInput
placeholder="Confirm password"
name="confirm"
isInvalid={isErrored}
isDisabled={isEncrypting}
/>
<FormErrorMessage>{errorMessage}</FormErrorMessage>
</FormControl>
<Button
size="lg"
mt={8}
w="full"
type="submit"
bg="black"
color="white"
boxShadow="2xl"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.02)'
}}
isLoading={isEncrypting}
loadingText="Preparing Your Note Wallet..."
>
{isEncrypting ? <Spinner size="sm" mr={2} /> : null}
{isEncrypting ? 'Loading' : 'Submit'}
</Button>
<Flex justifyContent="center" fontSize="sm" mt={5}>
<Link
color="blue.500"
onClick={() => setCurrentTab(0)}
ml={1}
>
<Flex alignItems="center">
<MdOutlineArrowBackIos size={15} />
<Text ml={1}>Go Back</Text>
</Flex>
</Link>
</Flex>
</Box>
</form>
</Box>
</TabPanel>
<TabPanel minHeight="400px">
<Flex direction="column" justify="space-between">
<Flex
width="full"
height="80%"
alignItems="center"
alignContent="center"
mb="160px"
>
<Button
as={Link}
onClick={() => download()}
colorScheme="blue"
size="lg"
w="full"
mt="20%"
bg="black"
color="white"
boxShadow="2xl"
border="4px solid black"
_hover={{
bg: 'white',
color: 'black',
borderColor: 'black',
transform: 'scaleX(1.01)',
textDecoration: 'none'
}}
>
<Flex alignItems="center">
<FiDownload size={20} />
<Text ml={2} textDecoration="none">
Download
</Text>
</Flex>
</Button>
</Flex>
<Flex height="10%" justifyContent="center" fontSize="xs">
<Link
color="gray.500"
onClick={() => setStage('Connect')}
ml={1}
>
<Text ml={1}>
Close this box after downloading & unlock your Note Wallet
with this secret.
</Text>
</Link>
</Flex>
</Flex>
</TabPanel>
</TabPanels>
</Tabs>
</Flex>
</Container>
);
}

View File

@@ -0,0 +1,289 @@
import React from 'react';
import {
Box,
Button,
Container,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Text,
FormControl,
FormLabel,
Flex
} from '@chakra-ui/react';
import { AiFillEye, AiFillEyeInvisible } from 'react-icons/ai';
import { atom, useAtom } from 'jotai';
import { hexZeroPad } from 'ethers/lib/utils';
import { NoteWalletV2 } from 'privacy-pools';
import {
activeIndexAtom,
noteAtom,
mnemonicAtom,
DefaultNote,
stageAtom
} from '../../state/atoms';
const isLoadingAtom = atom(false);
export default function Manage() {
const [showData, setShowData] = React.useState(false);
const [newIndex, setNewIndex] = React.useState<number>(NaN);
const [_stage, setStage] = useAtom(stageAtom);
const [note, setNote] = useAtom(noteAtom);
const [mnemonic, setMnemonic] = useAtom(mnemonicAtom);
const [activeIndex, setActiveIndex] = useAtom(activeIndexAtom);
const [isLoading, setIsLoading] = useAtom(isLoadingAtom);
const calculateNextKeys = (newIndex: number) => {
const _noteWallet = new NoteWalletV2(mnemonic, newIndex);
const _nextKeys = _noteWallet.interiorKeys[newIndex];
setNote({
index: newIndex,
commitment: _nextKeys.commitment,
secret: _nextKeys.secret
});
setActiveIndex(newIndex);
setTimeout(() => setIsLoading(false), 1);
};
const handleChange: React.Dispatch<React.SetStateAction<number>> = (
newIndex
) => {
if (isLoading) return;
if (Number.isNaN(newIndex)) return;
setIsLoading(true);
setTimeout(() => calculateNextKeys(Number(newIndex)), 200);
};
const logoutWallet = () => {
setMnemonic('');
setNote(DefaultNote);
setStage('Unlock');
};
return (
<Container maxW="98vw" minW="216px">
<Container>
<Flex direction="column">
<FormControl>
<FormLabel>
<Text fontWeight="bold">COMMITMENT</Text>
</FormLabel>
<Flex justifyContent="left" fontSize="sm" my={1}>
<Text color="gray.600">
The public commitment to be used in private proof of membership.
</Text>
</Flex>
<Text
size="lg"
py={4}
px={2}
borderRadius="10px"
bg="gray.100"
fontSize="sm"
fontWeight="bold"
fontFamily="monospace"
cursor="select"
sx={{ wordBreak: 'break-word' }}
>
{hexZeroPad(
'0x' + (note.commitment as any as BigInt).toString(16),
32
)}
</Text>
</FormControl>
<Box my={6}>
<form
onSubmit={(e) => {
e.preventDefault();
handleChange(newIndex);
}}
>
<FormLabel>
<Text fontWeight="bold">INDEX</Text>
</FormLabel>
<Flex justifyContent="center" fontSize="sm" my={1}>
<Text color="gray.600">
Part of the HD derivation path. Change this to use a different
commitment.
</Text>
</Flex>
<Flex w="full">
<NumberInput
w="20%"
size="sm"
defaultValue={activeIndex}
min={0}
max={1000}
onChange={(valueAsString, valueAsNumber) =>
setNewIndex(valueAsNumber)
}
allowMouseWheel
>
<Box bg="gray.50" borderRadius={8}>
<NumberInputField
color="gray.700"
bg="transparent"
border="none"
_active={{ border: 'none' }}
_focus={{ border: 'none' }}
_hover={{ border: 'none' }}
/>
</Box>
<NumberInputStepper>
<NumberIncrementStepper _hover={{ bg: 'gray.200' }} />
<NumberDecrementStepper _hover={{ bg: 'gray.200' }} />
</NumberInputStepper>
</NumberInput>
<Button
ml={10}
bg="black"
color="white"
border="none"
boxShadow="2xl"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.02)'
}}
w="80%"
size="sm"
type="submit"
disabled={
Number.isNaN(newIndex) || activeIndex === Number(newIndex)
}
>
Select
</Button>
</Flex>
</form>
</Box>
<Flex
w="full"
justifyContent="center"
direction="column"
alignItems="center"
mt={5}
>
{showData ? (
<Box bg="gray.50" w="full" borderRadius="20px" p={2}>
<FormControl>
<FormLabel>
<Text fontWeight="bold">SECRET</Text>
</FormLabel>
<Flex justifyContent="left" fontSize="sm" my={0}>
<Text color="gray.600">
Secret is the wallet&apos;s private key mod q.
</Text>
</Flex>
<Text
size="lg"
py={4}
px={2}
borderRadius="10px"
bg="gray.100"
fontSize="sm"
fontWeight="bold"
fontFamily="monospace"
cursor="select"
sx={{ wordBreak: 'break-word' }}
>
{`0x${(note.secret as any as BigInt).toString(16)}`}
</Text>
</FormControl>
<FormControl my={4}>
<FormLabel>
<Text fontWeight="bold">MNEMONIC</Text>
</FormLabel>
<Flex justifyContent="left" fontSize="sm" mt={0} mb={1}>
<Text color="gray.600">
{' '}
Only use a mnemonic generated by this page.
</Text>
</Flex>
<Text
size="lg"
py={4}
px={2}
borderRadius="10px"
bg="gray.100"
fontSize="sm"
fontWeight="bold"
fontFamily="monospace"
cursor="select"
sx={{ wordBreak: 'break-word' }}
>
{mnemonic.toString()}
</Text>
</FormControl>
</Box>
) : null}
<Button
onClick={() => setShowData(!showData)}
size="md"
w="full"
mt={5}
type="submit"
bg="gray.100"
color="black"
borderRadius={8}
boxShadow="2xl"
borderColor="red"
border="3px solid black"
_hover={{
bg: 'gray.700',
color: 'white',
borderColor: 'gray.600',
transform: 'scaleX(1.01)'
}}
>
{showData ? (
<Flex alignItems="center">
<AiFillEyeInvisible size={20} />
<Text ml={1} textDecoration="none">
Hide Secrets
</Text>
</Flex>
) : (
<Flex alignItems="center">
<AiFillEye size={20} />
<Text ml={1} textDecoration="none">
Show Secrets
</Text>
</Flex>
)}
</Button>
<Button
onClick={logoutWallet}
size="lg"
mt={5}
mb={5}
w="full"
type="submit"
bg="black"
color="white"
boxShadow="2xl"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.02)'
}}
>
LOGOUT
</Button>
</Flex>
</Flex>
</Container>
</Container>
);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { useAtom } from 'jotai';
import { stageAtom } from '../../state/atoms';
import Manage from './Manage';
import Create from './Create';
import Unlock from './Unlock';
import Connect from './Connect';
export function NoteWallet() {
const [stage] = useAtom(stageAtom);
return (
<>
{(function () {
switch (stage) {
case 'Connect':
return <Connect />;
case 'Manage':
return <Manage />;
case 'Create':
return <Create />;
case 'Unlock':
return <Unlock />;
default:
return null;
}
})()}
</>
);
}
export default NoteWallet;

View File

@@ -0,0 +1,141 @@
import React from 'react';
import {
Center,
Button,
HStack,
Text,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
IconButton,
Flex
} from '@chakra-ui/react';
import { FaArrowLeft } from 'react-icons/fa';
import { useAtom } from 'jotai';
import { hexZeroPad } from 'ethers/lib/utils';
import { stageAtom, activeIndexAtom } from '../../state';
import NoteWallet from './NoteWallet';
import { useNote } from '../../hooks';
import { growShrinkProps, pinchString } from '../../utils';
const DropdownIcon = () => (
<svg fill="none" height="7" width="14" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.75 1.54001L8.51647 5.0038C7.77974 5.60658 6.72026 5.60658 5.98352 5.0038L1.75 1.54001"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
xmlns="http://www.w3.org/2000/svg"
/>
</svg>
);
export function NoteWalletConnectButton() {
const { isOpen, onOpen, onClose } = useDisclosure();
const btnRef = React.useRef();
const { commitment } = useNote();
const [stage] = useAtom(stageAtom);
const [activeIndex] = useAtom(activeIndexAtom);
const [_stage, setStage] = useAtom(stageAtom);
return (
<>
<Center>
<Button
boxShadow="xl"
bg="black"
color="white"
ref={btnRef as unknown as React.LegacyRef<HTMLButtonElement>}
variant="outline"
onClick={onOpen}
borderRadius={15}
border="none"
w="full"
height="40px"
px={commitment ? 0.5 : 1.5}
{...growShrinkProps}
>
<HStack w="full">
{!commitment.eq(0) ? (
<Flex
height={{ base: '35px', md: '36px' }}
justifyContent="space-between"
w="full"
align="center"
>
<Text px={{ base: 1, md: 2 }}>Note {activeIndex}</Text>
<HStack
w="full"
px={2}
bg="gray.600"
color="white"
justifyContent="space-evenly"
h="full"
borderRadius={8}
>
<Text>
{pinchString(
hexZeroPad(commitment.toHexString(), 32),
[4, 6]
)}
</Text>
<DropdownIcon />
</HStack>
</Flex>
) : (
<>
<Text px={4} textAlign="center" w="full">
Connect Note Wallet
</Text>
</>
)}
</HStack>
</Button>
</Center>
<Modal
isOpen={isOpen}
onClose={onClose}
size="xl"
isCentered
motionPreset="slideInBottom"
>
<ModalOverlay />
<ModalContent borderRadius="xl" bg="white">
<ModalHeader textAlign="center">
<Flex alignItems="center">
{_stage !== 'Connect' && _stage !== 'Manage' && (
<IconButton
aria-label="Go back"
icon={<FaArrowLeft />}
onClick={() => setStage('Connect')}
bg="white"
mt={-2}
ml={-4}
/>
)}
<Center flex="1">{stage} your Note Wallet</Center>
</Flex>
</ModalHeader>
<ModalCloseButton
size="md"
mt={1}
borderRadius="50%"
{...growShrinkProps}
/>
<ModalBody px={{ base: 0, md: 5 }}>
<NoteWallet />
</ModalBody>
</ModalContent>
</Modal>
</>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { Button, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
export default function PasswordInput({
placeholder,
name,
isInvalid,
isDisabled
}: {
placeholder: string;
name: string;
isInvalid: boolean;
isDisabled?: boolean;
}) {
const [show, setShow] = React.useState(false);
const handleClick = () => setShow(!show);
return (
<InputGroup size="lg" alignItems="center" my={2}>
<Input
size="lg"
name={name}
type={show ? 'text' : 'password'}
placeholder={placeholder}
isInvalid={isInvalid || false}
isDisabled={isDisabled}
fontSize="sm"
color="gray.800"
bg="gray.50"
focusBorderColor="gray.400"
alignItems="center"
/>
<InputRightElement width="4.5rem">
<Button size="md" onClick={handleClick} variant="ghost">
{show ? <ViewIcon /> : <ViewOffIcon />}
</Button>
</InputRightElement>
</InputGroup>
);
}

View File

@@ -0,0 +1,278 @@
import React, { useState } from 'react';
import {
Box,
Button,
Center,
Container,
FormControl,
Input,
Link,
InputGroup,
Progress,
Text,
Flex
} from '@chakra-ui/react';
import { AiOutlineFileText } from 'react-icons/ai';
import PasswordInput from './PasswordInput';
import { useAtom } from 'jotai';
import {
noteAtom,
mnemonicAtom,
stageAtom,
activeIndexAtom,
encryptedJsonAtom,
EncryptedJson
} from '../../state/atoms';
import * as ethers from 'ethers';
import { NoteWalletV2 } from 'privacy-pools';
const validateEncryptedMnemonic = (_encryptedMnemonic: EncryptedJson) => {
if (typeof _encryptedMnemonic !== 'object') {
return false;
}
['address', 'crypto', 'x-ethers'].map((expectedKey) => {
if (!Object.keys(_encryptedMnemonic).includes(expectedKey)) return false;
});
return true;
};
export default function Unlock() {
const [decryptProgress, setDecryptProgress] = useState(0);
const [isDecrypting, setIsDecrypting] = useState(false);
const [decryptFailed, setDecryptFailed] = useState(false);
const [_note, setNote] = useAtom(noteAtom);
const [_mnemonic, setMnemonic] = useAtom(mnemonicAtom);
const [activeIndex] = useAtom(activeIndexAtom);
const [encryptedJson, setEncryptedJson] = useAtom(encryptedJsonAtom);
const [_, setStage] = useAtom(stageAtom);
const handleFileChange = (e: any) => {
console.log('here1');
e.preventDefault();
const file = e.target.files[0];
if (typeof file === 'undefined') {
setEncryptedJson({});
console.log('here2');
return;
}
const fileReader = new FileReader();
fileReader.onloadend = () => {
try {
console.log('here3');
let _encryptedMnemonic = JSON.parse(fileReader.result as string);
if (typeof _encryptedMnemonic === 'string') {
_encryptedMnemonic = JSON.parse(_encryptedMnemonic);
}
if (validateEncryptedMnemonic(_encryptedMnemonic)) {
setEncryptedJson(_encryptedMnemonic);
} else {
setEncryptedJson({});
throw new Error('Invalid mnemonic file!');
}
console.log('here4');
} catch (e) {
console.log(e);
alert('Failed to parse the JSON file.');
console.log('here4');
setEncryptedJson({});
}
};
fileReader.readAsText(file);
};
const handleSubmit = (e: any) => {
e.preventDefault();
if (Object.keys(encryptedJson).length === 0) {
return;
}
const formData = new FormData(e.target);
const [_password] = [...formData.values()];
if (_password) {
setDecryptFailed(false);
setIsDecrypting(true);
ethers.Wallet.fromEncryptedJson(
JSON.stringify(encryptedJson),
_password.toString(),
setDecryptProgress
)
.then((_wallet) => {
const _noteWallet = new NoteWalletV2(
_wallet.mnemonic.phrase,
activeIndex
);
const _nextKeys = _noteWallet.interiorKeys[activeIndex];
setMnemonic(_wallet.mnemonic.phrase);
setNote({
index: activeIndex,
commitment: _nextKeys.commitment,
secret: _nextKeys.secret
});
})
.then(() => setTimeout(() => setIsDecrypting(false), 1))
.then(() => setTimeout(() => setStage('Manage'), 1))
.catch((err) => {
console.log('here4');
setDecryptFailed(true);
setIsDecrypting(false);
alert('Failed to decrypt the mnemonic.');
});
}
};
return (
<Container>
<Center h="100%">
<Container pb={4} borderRadius={8} fontWeight="bold">
<form onSubmit={handleSubmit}>
<Box>
<Box>
<Input
id={'fileElementId'}
type="file"
accept="json"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
<Flex h="100%" pt={4} alignItems="center">
{typeof encryptedJson?.address === 'undefined' ? (
<Button
w="100%"
onClick={() =>
document.getElementById('fileElementId')?.click()
}
size={{ base: 'md', md: 'lg' }}
>
Select Encrypted File
</Button>
) : (
<Box w="full" my={2}>
<Text color="black" my={2} fontSize="md">
Select Your Encrypted Json File
</Text>
<Flex align="center">
<InputGroup size="lg" w="75%">
<Input
readOnly
size="md"
bg="gray.50"
type="text"
fontSize="sm"
value={encryptedJson.address}
focusBorderColor="gray.400"
/>
</InputGroup>
<Button
onClick={() =>
document.getElementById('fileElementId')?.click()
}
disabled={isDecrypting}
size="md"
bg="black"
color="white"
w="25%"
mx={2}
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.01)'
}}
>
<Flex alignItems="center" justifyContent="center">
<Text
fontSize={{
base: 'xs',
md: 'md'
}}
>
Choose File
</Text>
{/* <AiOutlineFileText/> */}
</Flex>
</Button>
</Flex>
</Box>
)}
</Flex>
</Box>
<Box my={2}>
<Flex h="100%" alignItems="center">
<Box w="full">
<Text color="black" my={2} fontSize="md">
Password
</Text>
<FormControl>
<PasswordInput
name="decryptPassword"
placeholder="Enter password"
isInvalid={decryptFailed}
isDisabled={isDecrypting}
/>
</FormControl>
</Box>
</Flex>
</Box>
<Box mt={10}>
<Flex alignItems="center" justifyContent="center" py={2}>
{decryptFailed && (
<Text fontSize="xs" fontWeight="600" color="red.500" mt={1}>
FAILED TO DECRYPT: Invalid Json File or password value
provided.
</Text>
)}
</Flex>
<Flex h="100%" alignItems="center">
<Button
disabled={isDecrypting}
size="lg"
w="full"
type="submit"
bg="black"
color="white"
boxShadow="2xl"
_hover={{
bg: 'black',
color: 'white',
borderColor: 'black',
transform: 'scaleX(1.05)'
}}
isDisabled={isDecrypting}
>
UNLOCK
</Button>
</Flex>
</Box>
{isDecrypting && (
<Box mt={2}>
<Progress value={decryptProgress * 100} size="md" mt={4} />
<Container centerContent px={0} pt={2} textAlign="center">
<Text fontSize="xs" color="gray.600">
Attempting to decrypt wallet.If the password is wrong,
this operation will fail
</Text>
</Container>
</Box>
)}
</Box>
</form>
<Flex justifyContent="center" fontSize="sm" mt={5}>
<Text color="gray.600">Don&apos;t Have A Note Wallet? </Text>
<Link color="blue.500" onClick={() => setStage('Create')} ml={1}>
Create One
</Link>
</Flex>
</Container>
</Center>
</Container>
);
}

View File

@@ -0,0 +1,2 @@
export { NoteWallet } from './NoteWallet';
export { NoteWalletConnectButton } from './NoteWalletConnectButton';

View File

@@ -0,0 +1,184 @@
import {
Container,
Heading,
Link,
Stat,
HStack,
VStack,
StatLabel,
Text,
StatHelpText,
Select,
Spinner
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import NextLink from 'next/link';
import { useBalance, useContractReads, useNetwork } from 'wagmi';
import { useAtom } from 'jotai';
import { useContractAddress, useOptions } from '../hooks';
import { assetAtom, denominationAtom } from '../state';
import { privacyPoolABI } from '../constants';
import { pinchString } from '../utils';
export function PoolExplorer() {
const [asset, setAsset] = useAtom(assetAtom);
const [denomination, setDenomination] = useAtom(denominationAtom);
const { chain } = useNetwork();
const { contractAddress } = useContractAddress();
const { assetOptions, denominationOptions } = useOptions();
const {
data: poolData,
isError: isPoolDataError,
isLoading: isPoolDataLoading
} = useContractReads({
contracts: [
{
address: contractAddress,
abi: privacyPoolABI,
functionName: 'getLatestRoot'
},
{
address: contractAddress,
abi: privacyPoolABI,
functionName: 'currentLeafIndex'
}
]
});
const {
data: balanceData,
isLoading: isBalanceLoading,
isError: isBalanceError
} = useBalance({
address: contractAddress as `0x${string}`
});
return (
<Container minW="216px" maxW="98vw">
<Container
my={8}
p={4}
pb={2}
bg="white"
borderRadius={16}
boxShadow="2xl"
>
<VStack align="center" mb={2}>
<Heading fontSize="2xl">
{denomination} {asset} Pool
</Heading>
<Link
as={NextLink}
isExternal
href={`${chain?.blockExplorers?.default.url}/address/${contractAddress}`}
>
<Text color="gray.800" fontWeight="bold" wordBreak="break-all">
{pinchString(contractAddress, 8)} <ExternalLinkIcon />
</Text>
</Link>
<Container px={0} py={2}>
<HStack w="full">
<Select
size="md"
bg="gray.100"
onChange={(e) => setDenomination(e.target.value)}
defaultValue={denomination}
>
{denominationOptions.map((_denomination, i) => (
<option
value={_denomination}
key={`${_denomination}-${i}-option`}
>
{_denomination.toString()}
</option>
))}
</Select>
<Select
size="md"
bg="gray.100"
onChange={(e) => setAsset(e.target.value)}
defaultValue={asset}
>
{assetOptions.map((_asset, i) => (
<option value={_asset} key={`${_asset}-${i}-option`}>
{_asset.toString()}
</option>
))}
</Select>
</HStack>
</Container>
</VStack>
<HStack justify="space-between" pb={2}>
<VStack align="flex-start">
<Stat>
<StatLabel fontSize="lg">Pool Balance</StatLabel>
<Text fontWeight="bold" fontSize="2xl">
{isBalanceLoading ? (
<Spinner />
) : isBalanceError ? (
'--'
) : (
balanceData?.formatted
)}
</Text>
<StatHelpText fontSize="lg">
{balanceData?.symbol === asset
? asset
: `${balanceData?.symbol} (${asset})`}
</StatHelpText>
</Stat>
</VStack>
<VStack align="flex-end">
<Stat>
<StatLabel fontSize="lg">Total</StatLabel>
<Text fontWeight="bold" fontSize="2xl">
{isPoolDataLoading ? (
<Spinner />
) : isPoolDataError ? (
'--'
) : (
(
((poolData as [string, BigInt]) || [null, null])[1] || 0
).toString()
)}
</Text>
<StatHelpText
textAlign="right"
fontSize="lg"
wordBreak="break-word"
>
# of Deposits
</StatHelpText>
</Stat>
</VStack>
</HStack>
<Stat>
<StatLabel fontSize="lg">Root</StatLabel>
<Text fontWeight="bold" fontSize="lg" w="50%">
{isPoolDataLoading ? (
<Spinner />
) : isPoolDataError ? (
'--'
) : (
pinchString(
(
((poolData as [string, BigInt]) || [null, null])[0] || 0
).toString(),
10
)
)}
</Text>
<StatHelpText fontSize="sm">Most Recent Root</StatHelpText>
</Stat>
</Container>
</Container>
);
}
export default PoolExplorer;

View File

@@ -0,0 +1,224 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Heading,
Text,
Switch,
Container,
VStack,
HStack,
Link,
Tooltip
} from '@chakra-ui/react';
import { ExternalLinkIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { hexZeroPad } from 'ethers/lib/utils';
import * as _ from 'lodash';
import { AccessList, SubsetData } from 'pools-ts';
import NextLink from 'next/link';
import { useNetwork } from 'wagmi';
import { useAtom } from 'jotai';
import { commitmentsAtom } from '../../state';
import { useCommitments, useNote, useAccessList } from '../../hooks';
import { pinchString, growShrinkProps } from '../../utils';
export function SubsetMaker() {
const [subsetData, setSubsetData] = useState<SubsetData>([]);
const [commitments] = useAtom(commitmentsAtom);
const { chain } = useNetwork();
const { commitment } = useNote();
// const { commitments } = useCommitments()
const { accessList, setAccessList } = useAccessList();
useEffect(() => {
if (subsetData.length === 0) {
if (accessList.length >= 30) {
setSubsetData(
accessList.getWindow(accessList.length - 30, accessList.length)
);
} else if (accessList.length > 0) {
setSubsetData(accessList.getWindow(0, accessList.length));
}
}
}, [subsetData, accessList]);
const syncChecked = (e: any) => {
e.preventDefault();
const index = Number(e.target.id);
if (index >= subsetData.length) return;
const _subsetData = [...subsetData];
if (_subsetData[index] === 1) {
_subsetData[e.target.id] = 0;
} else {
_subsetData[e.target.id] = 1;
}
setSubsetData(_subsetData);
};
const syncAccessList = () => {
let start: number = 0;
let end: number = accessList.length;
if (accessList.length >= 30) {
start = accessList.length - 30;
}
if (!_.isEqual(accessList.getWindow(start, end), subsetData)) {
const _accessList = AccessList.fromJSON(accessList.toJSON());
_accessList.setWindow(start, end, subsetData);
setAccessList(_accessList);
}
};
const isSyncDisabled = _.isEqual(
subsetData,
accessList.getWindow(
accessList.length < 30 ? 0 : accessList.length - 30,
accessList.length
)
);
return (
<Container px={2} centerContent gap={2}>
<HStack mb={4}>
<Tooltip label="Exclude some of the most recent deposits by optionally checking the corresponding slider. The demo blocklist contains a list of the most recent deposits, 30 at most.">
<QuestionOutlineIcon />
</Tooltip>
<Heading size="md">Exclude some deposits</Heading>
</HStack>
<Container bg="blue.50" borderRadius={8} p={4}>
<Heading size="sm">Subset Root</Heading>
<Text
fontSize="xs"
color="blue.800"
fontWeight="bold"
mt={2}
textAlign="center"
>
{accessList?.root.toHexString()}
</Text>
{!isSyncDisabled && (
<HStack justify="center" mt={2}>
<Container centerContent gap={2} pt={2}>
<Text fontSize="xs" color="orange.500">
Warning: unsynced changes!
</Text>
<Button
size="xs"
colorScheme="blue"
onClick={syncAccessList}
isDisabled={isSyncDisabled}
>
Sync
</Button>
</Container>
</HStack>
)}
</Container>
<Container
mt={2}
w="full"
bg="blue.50"
borderRadius={8}
centerContent
p={4}
>
<VStack w="full">
<HStack w="full" mb={2}>
<Container w="10%" centerContent p={0}>
<Heading size="sm">#</Heading>
</Container>
<Container w="20%" centerContent p={0}>
<Heading size="sm">block?</Heading>
</Container>
<Container w="30%" centerContent p={0}>
<Heading size="sm">sender</Heading>
</Container>
<Container w="40%" centerContent p={0}>
<Heading size="sm">commitment</Heading>
</Container>
</HStack>
<Container p={0} centerContent overflowY="auto" maxH="50vh">
{!commitment.eq(0) &&
commitments.length > 0 &&
(commitments.length > 30
? commitments.slice(commitments.length - 30)
: commitments
).map((commitmentData, i) => {
return (
<HStack key={`row-${i}-${commitmentData.leafIndex}`} w="full">
<Container centerContent p={0} w="10%">
<Text
key={`leafIndex-${i}-${commitmentData.leafIndex}`}
fontSize="sm"
textAlign="left"
>
{commitmentData.leafIndex.toString()}
</Text>
</Container>
<Container centerContent p={0} w="20%">
<Switch
key={`switch-${i}-${commitmentData.leafIndex}`}
id={`${i}`}
isDisabled={commitment.eq(commitmentData.commitment)}
isChecked={Boolean(subsetData[i])}
onChange={syncChecked}
/>
</Container>
<Container
key={`container-${i}-${commitmentData.leafIndex}`}
w="30%"
p={0}
centerContent
>
<Link
key={`link-${i}-${commitmentData.leafIndex}`}
as={NextLink}
href={`${chain?.blockExplorers?.default.url}/address/${commitmentData.sender}`}
isExternal
{...growShrinkProps}
>
<HStack>
<Text
key={`sender-${i}-${commitmentData.sender}`}
color="blue.700"
fontSize="sm"
textAlign="left"
{...growShrinkProps}
>
{pinchString(commitmentData.sender.toString(), 4)}
</Text>
<Text color="blue.700" fontSize="sm" pb="4px">
<ExternalLinkIcon key={`external-link-icon-${i}`} />
</Text>
</HStack>
</Link>
</Container>
<Container centerContent p={0} w="40%">
<Text
key={`commitment-${i}-${commitmentData.commitment}`}
fontSize="sm"
textAlign="left"
>
{pinchString(
hexZeroPad(commitmentData.commitment.toString(), 32),
6
)}
</Text>
</Container>
</HStack>
);
})}
</Container>
</VStack>
</Container>
</Container>
);
}
export default SubsetMaker;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import {
Button,
VStack,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure
} from '@chakra-ui/react';
import SubsetMaker from './SubsetMaker';
export function SubsetMakerButton() {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button
onClick={onOpen}
bg="gray.200"
color="black"
mx={2}
fontSize="lg"
fontWeight="bold"
_hover={{
bg: 'white',
color: 'black',
borderColor: 'gray.600',
transform: 'scaleX(1.05)'
}}
w={['75%', '50%']}
>
Subset Maker
</Button>
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
<ModalOverlay />
<ModalContent pb={4}>
<ModalHeader>Subset Maker Demo</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack align="center" justify="center">
<SubsetMaker key="subset-maker" />
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</>
);
}
export default SubsetMakerButton;

View File

@@ -0,0 +1 @@
export { SubsetMakerButton } from './SubsetMakerButton';

8
src/components/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export { NoteWalletConnectButton } from './NoteWallet';
export { default as CenteredPage } from './CenteredPage';
export { default as DappLayout } from './DappLayout';
export { default as HomeLayout } from './HomeLayout';
export { default as HeaderNavbar } from './HeaderNavbar';
export { default as PoolExplorer } from './PoolExplorer';
export { SubsetMakerButton } from './SubsetMaker';
export { AssetDenominationBar } from './AssetDenominationBar';

44
src/constants/index.ts Normal file
View File

@@ -0,0 +1,44 @@
export { privacyPoolABI } from './privacyPoolAbi';
export { subsetRegistryABI } from './subsetRegistryAbi';
interface Assets {
[chainId: number]: string[];
}
interface Denominations {
[chainId: number]: { [asset: string]: number[] };
}
interface Contracts {
[chainId: number]: { [assetDenominationPair: string]: string };
}
export const assets: Assets = {
5: ['ETH'],
420: ['ETH']
};
export const denominations: Denominations = {
5: {
ETH: [0.001, 0.01]
},
420: {
ETH: [0.001, 0.01]
}
};
export const subsetRegistries: { [chainId: number]: string } = {
5: '0x5B27a0d86fa25bf74A77f0d0841d292eD4B6f992',
420: '0xa4410556507e44EDa497Fe5051eb37F8aD2C4104'
};
export const contracts: Contracts = {
5: {
'ETH-0.001': '0xeeB3445BB3702B1aE830f6fe02BcFeF082860468',
'ETH-0.01': '0xC1e42b18Ba0c454f32D437c397FC96a51dB3556d'
},
420: {
'ETH-0.001': '0x3fB005c1A83FCF63A87fC584aC2a5c67FB38F880',
'ETH-0.01': '0x13B6BD28d27a33c14E3B7f95185Ec7a091C1F2de'
}
};

View File

@@ -0,0 +1,368 @@
export const privacyPoolABI = [
{
inputs: [
{
internalType: 'address',
name: 'poseidon',
type: 'address'
},
{
internalType: 'uint256',
name: '_denomination',
type: 'uint256'
}
],
stateMutability: 'nonpayable',
type: 'constructor'
},
{
inputs: [],
name: 'IncrementalMerkleTree__MerkleTreeCapacity',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__FeeExceedsDenomination',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__InvalidZKProof',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__MsgValueInvalid',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__NoteAlreadySpent',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__UnknownRoot',
type: 'error'
},
{
inputs: [],
name: 'PrivacyPool__ZeroAddress',
type: 'error'
},
{
inputs: [],
name: 'ProofLib__ECAddFailed',
type: 'error'
},
{
inputs: [],
name: 'ProofLib__ECMulFailed',
type: 'error'
},
{
inputs: [],
name: 'ProofLib__ECPairingFailed',
type: 'error'
},
{
inputs: [],
name: 'ProofLib__GteSnarkScalarField',
type: 'error'
},
{
inputs: [],
name: 'ProofLib__PairingLengthsFailed',
type: 'error'
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'bytes32',
name: 'commitment',
type: 'bytes32'
},
{
indexed: false,
internalType: 'uint256',
name: 'denomination',
type: 'uint256'
},
{
indexed: false,
internalType: 'uint256',
name: 'leafIndex',
type: 'uint256'
},
{
indexed: false,
internalType: 'uint256',
name: 'timestamp',
type: 'uint256'
}
],
name: 'Deposit',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: 'address',
name: 'recipient',
type: 'address'
},
{
indexed: true,
internalType: 'address',
name: 'relayer',
type: 'address'
},
{
indexed: true,
internalType: 'bytes32',
name: 'subsetRoot',
type: 'bytes32'
},
{
indexed: true,
internalType: 'bytes32',
name: 'nullifier',
type: 'bytes32'
},
{
indexed: false,
internalType: 'uint256',
name: 'fee',
type: 'uint256'
}
],
name: 'Withdrawal',
type: 'event'
},
{
inputs: [],
name: 'LEVELS',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'ROOTS_CAPACITY',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'currentLeafIndex',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'denomination',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'bytes32',
name: 'commitment',
type: 'bytes32'
}
],
name: 'deposit',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
stateMutability: 'payable',
type: 'function'
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
name: 'filledSubtrees',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'getLatestRoot',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'hasher',
outputs: [
{
internalType: 'contract IPoseidon',
name: '',
type: 'address'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'bytes32',
name: 'root',
type: 'bytes32'
}
],
name: 'isKnownRoot',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32'
}
],
name: 'nullifiers',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256'
}
],
name: 'roots',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'uint256[8]',
name: 'flatProof',
type: 'uint256[8]'
},
{
internalType: 'bytes32',
name: 'root',
type: 'bytes32'
},
{
internalType: 'bytes32',
name: 'subsetRoot',
type: 'bytes32'
},
{
internalType: 'bytes32',
name: 'nullifier',
type: 'bytes32'
},
{
internalType: 'address',
name: 'recipient',
type: 'address'
},
{
internalType: 'address',
name: 'relayer',
type: 'address'
},
{
internalType: 'uint256',
name: 'fee',
type: 'uint256'
}
],
name: 'withdraw',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool'
}
],
stateMutability: 'nonpayable',
type: 'function'
}
];

View File

@@ -0,0 +1,171 @@
export const subsetRegistryABI = [
{
inputs: [],
stateMutability: 'nonpayable',
type: 'constructor'
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'bytes32',
name: 'subsetRoot',
type: 'bytes32'
},
{
indexed: true,
internalType: 'bytes32',
name: 'nullifier',
type: 'bytes32'
},
{
indexed: true,
internalType: 'address',
name: 'pool',
type: 'address'
},
{
indexed: false,
internalType: 'uint256',
name: 'accessType',
type: 'uint256'
},
{
indexed: false,
internalType: 'uint256',
name: 'bitLength',
type: 'uint256'
},
{
indexed: false,
internalType: 'bytes',
name: 'subsetData',
type: 'bytes'
}
],
name: 'Subset',
type: 'event'
},
{
inputs: [
{
internalType: 'address[]',
name: 'pools',
type: 'address[]'
}
],
name: 'addPools',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [],
name: 'owner',
outputs: [
{
internalType: 'address',
name: '',
type: 'address'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address'
}
],
name: 'privacyPools',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool'
}
],
stateMutability: 'view',
type: 'function'
},
{
inputs: [
{
internalType: 'address[]',
name: 'pools',
type: 'address[]'
}
],
name: 'removePools',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{
internalType: 'address',
name: 'privacyPool',
type: 'address'
},
{
internalType: 'uint256',
name: 'accessType',
type: 'uint256'
},
{
internalType: 'uint256',
name: 'bitLength',
type: 'uint256'
},
{
internalType: 'bytes',
name: 'subsetData',
type: 'bytes'
},
{
internalType: 'uint256[8]',
name: 'flatProof',
type: 'uint256[8]'
},
{
internalType: 'bytes32',
name: 'root',
type: 'bytes32'
},
{
internalType: 'bytes32',
name: 'subsetRoot',
type: 'bytes32'
},
{
internalType: 'bytes32',
name: 'nullifier',
type: 'bytes32'
},
{
internalType: 'address',
name: 'recipient',
type: 'address'
},
{
internalType: 'address',
name: 'relayer',
type: 'address'
},
{
internalType: 'uint256',
name: 'fee',
type: 'uint256'
}
],
name: 'withdrawAndRecord',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
];

12
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,12 @@
export { useNote } from './useNote';
export { useContractAddress } from './useContractAddress';
export { default as useDebounce } from './useDebounce';
export { useOptions } from './useOptions';
export { useAccessList } from './useAccessList';
export { useDepositsTree } from './useDepositsTree';
export { useExistingCommitments } from './useExistingCommitments';
export { useExplorerData } from './useExplorerData';
export { useZKeys } from './useZKeys';
export { useCommitments } from './useCommitments';
export { useSubsetDataByNullifier } from './useSubsetDataByNullifier';
export { useSubsetRoots } from './useSubsetRoots';

View File

@@ -0,0 +1,49 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';
import { AccessList } from 'pools-ts';
import { accessListAtom } from '../state';
import { useCommitments, useDebounce } from '../hooks';
export function useAccessList() {
const [accessList, setAccessList] = useAtom(accessListAtom);
const { commitments } = useCommitments();
const debouncedCommitments = useDebounce(commitments, 500);
useEffect(() => {
// initialize or extend
if (
Array.isArray(debouncedCommitments) &&
debouncedCommitments.length > 0
) {
if (
accessList.length === 0 ||
accessList.length > debouncedCommitments.length
) {
const _accessList = AccessList.fullEmpty({
accessType: 'blocklist',
subsetLength: debouncedCommitments.length
});
setAccessList(_accessList);
} else if (accessList.length < debouncedCommitments.length) {
let _accessList: AccessList;
if (debouncedCommitments.length >= 30) {
_accessList = AccessList.fullEmpty({
accessType: 'blocklist',
subsetLength: debouncedCommitments.length
});
let start = _accessList.length - 30;
let end = accessList.length;
if (start >= 0 && start < end) {
_accessList.setWindow(start, end, accessList.getWindow(start, end));
}
} else {
_accessList = AccessList.fromJSON(accessList.toJSON());
_accessList.extend(debouncedCommitments.length);
}
setAccessList(_accessList);
}
}
}, [accessList, debouncedCommitments, setAccessList]);
return { accessList, setAccessList };
}

View File

@@ -0,0 +1,28 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';
import { useQuery } from 'urql';
import { commitmentsAtom } from '../state/atoms';
import { useContractAddress } from '../hooks';
import { CommitmentsQuery, CommitmentsQueryDocument } from '../query';
export function useCommitments() {
const [commitments, setCommitments] = useAtom(commitmentsAtom);
const { contractAddress } = useContractAddress();
const [result, executeCommitmentsQuery] = useQuery<CommitmentsQuery>({
query: CommitmentsQueryDocument,
variables: {
lastLeafIndex: -1,
contractAddress
},
requestPolicy: 'cache-and-network'
});
useEffect(() => {
if (Array.isArray(result?.data?.commitments)) {
setCommitments(result!.data!.commitments);
}
}, [result, setCommitments]);
return { commitments, executeCommitmentsQuery };
}

View File

@@ -0,0 +1,18 @@
import { useNetwork } from 'wagmi';
import { contracts, subsetRegistries } from '../constants';
import { useAtom } from 'jotai';
import { assetAtom, denominationAtom } from '../state/atoms';
export function useContractAddress() {
const [asset] = useAtom(assetAtom);
const [denomination] = useAtom(denominationAtom);
const { chain } = useNetwork();
if (!chain || !Object.keys(contracts).includes(chain.id.toString()))
return { contractAddress: '', subsetRegistry: '' };
return {
contractAddress: contracts[chain.id][`${asset}-${denomination}`],
subsetRegistry: subsetRegistries[chain.id]
};
}

17
src/hooks/useDebounce.ts Normal file
View File

@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';
function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;

View File

@@ -0,0 +1,38 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';
import { MerkleTree } from 'pools-ts';
import { hexZeroPad } from 'ethers/lib/utils';
import { depositsTreeAtom, Commitment } from '../state';
import { useCommitments, useDebounce } from '../hooks';
export function useDepositsTree() {
const [depositsTree, setDepositsTree] = useAtom(depositsTreeAtom);
const { commitments } = useCommitments();
const debouncedCommitments = useDebounce<Commitment[]>(commitments, 500);
useEffect(() => {
if (debouncedCommitments && debouncedCommitments.length > 0) {
if (depositsTree.length === 0) {
const tree = new MerkleTree({
leaves: debouncedCommitments.map(({ commitment }) =>
hexZeroPad(commitment.toString(), 32)
),
zeroString: 'empty'
});
setDepositsTree(tree);
} else if (depositsTree.length < debouncedCommitments.length) {
const tree = MerkleTree.fromJSON(depositsTree.toJSON());
for (
let i = depositsTree.length;
i < debouncedCommitments.length;
i++
) {
tree.insert(debouncedCommitments[i].commitment);
}
setDepositsTree(tree);
}
}
}, [debouncedCommitments, depositsTree, setDepositsTree]);
return { depositsTree };
}

View File

@@ -0,0 +1,37 @@
import { BigNumber, BigNumberish } from 'ethers';
import { poseidon } from 'pools-ts';
import { Commitment } from '../state';
import { useCommitments, useNote, useDebounce } from '../hooks';
export function useExistingCommitments() {
const { commitments } = useCommitments();
const { commitment, secret } = useNote();
const debouncedCommitments = useDebounce<Commitment[]>(commitments, 500);
let leafIndexToIndex: { [leafIndex: number]: number } = {};
let existingCommitments: (Commitment & { nullifier: string })[] = [];
if (!debouncedCommitments || !commitment || !secret) {
return { existingCommitments, leafIndexToIndex };
}
for (let i = 0; i < debouncedCommitments.length; i++) {
const commitmentData = debouncedCommitments[i];
if (
BigNumber.from(commitmentData.commitment).eq(commitment as BigNumberish)
) {
leafIndexToIndex[Number(commitmentData.leafIndex)] =
existingCommitments.length;
existingCommitments.push({
nullifier: poseidon([
secret,
1,
commitmentData.leafIndex
] as BigNumberish[]).toHexString(),
...commitmentData
});
}
}
return { existingCommitments, leafIndexToIndex };
}

View File

@@ -0,0 +1,69 @@
import { useState, useEffect } from 'react';
import { useAtom } from 'jotai';
import { AccessList } from 'pools-ts';
import { recentWithdrawalAtom, Commitment } from '../state';
import {
useCommitments,
useDebounce,
useSubsetDataByNullifier
} from '../hooks';
export function useExplorerData() {
const { commitments } = useCommitments();
const [recentWithdrawal] = useAtom(recentWithdrawalAtom);
const debouncedCommitments = useDebounce(commitments, 500);
const debouncedRecentWithdrawal = useDebounce(recentWithdrawal, 500);
const { subsetMetadata } = useSubsetDataByNullifier();
const { nullifier, subsetRoot } = debouncedRecentWithdrawal;
const { accessType, bitLength, subsetData: data } = subsetMetadata;
if (
!nullifier ||
!subsetRoot ||
!debouncedCommitments.length ||
!accessType ||
isNaN(bitLength) ||
data.length === 0
) {
return {
accessList: AccessList.fullEmpty({
accessType: 'blocklist',
subsetLength: 0
}),
includedDeposits: [],
excludedDeposits: []
};
}
const accessList = new AccessList({
accessType,
bytesData: { bitLength, data }
});
let includedDeposits: Commitment[];
let excludedDeposits: Commitment[];
if (commitments.length >= accessList.length) {
let start = 0;
const end = accessList.length;
if (end > 30) {
start = end - 30;
}
const c = commitments.slice(start, end);
includedDeposits = c.filter(
({ leafIndex }) => accessList.subsetData[Number(leafIndex)] === 0
);
excludedDeposits = c.filter(
({ leafIndex }) => accessList.subsetData[Number(leafIndex)] === 1
);
} else {
includedDeposits = [];
excludedDeposits = [];
}
return {
accessList,
includedDeposits,
excludedDeposits
};
}

12
src/hooks/useNote.ts Normal file
View File

@@ -0,0 +1,12 @@
import { BigNumber } from 'ethers';
import { useAtom } from 'jotai';
import { noteAtom, Note } from '../state/atoms';
export function useNote(): Note {
const [note] = useAtom(noteAtom);
return {
index: note.index,
commitment: BigNumber.from(note.commitment.toString()),
secret: BigNumber.from(note.secret.toString())
};
}

32
src/hooks/useOptions.ts Normal file
View File

@@ -0,0 +1,32 @@
import { useNetwork } from 'wagmi';
import { assets, denominations } from '../constants';
import { useAtom } from 'jotai';
import { assetAtom } from '../state/atoms';
export function useOptions() {
const [asset] = useAtom(assetAtom);
const { chain } = useNetwork();
let assetOptions: string[] = [];
let denominationOptions: number[] = [];
if (!chain || !chain.id) {
return { assetOptions, denominationOptions };
}
if (
Object.keys(assets).includes(chain.id.toString()) &&
Array.isArray(assets[chain.id])
) {
assetOptions = assets[chain.id];
}
if (
Object.keys(denominations).includes(chain.id.toString()) &&
Array.isArray(denominations[chain.id][asset])
) {
denominationOptions = denominations[chain.id][asset];
}
return { assetOptions, denominationOptions };
}

View File

@@ -0,0 +1,50 @@
import { useEffect } from 'react';
import { useQuery } from 'urql';
import { useAtom } from 'jotai';
import { recentWithdrawalAtom, subsetMetadataAtom } from '../state';
import { useContractAddress } from '../hooks';
import {
SubsetDataByNullifierQuery,
SubsetDataByNullifierQueryDocument
} from '../query';
export function useSubsetDataByNullifier() {
const [recentWithdrawal] = useAtom(recentWithdrawalAtom);
const [subsetMetadata, setSubsetMetadata] = useAtom(subsetMetadataAtom);
const { contractAddress } = useContractAddress();
const [result, executeSubsetDatasQuery] =
useQuery<SubsetDataByNullifierQuery>({
query: SubsetDataByNullifierQueryDocument,
variables: {
contractAddress,
nullifier: recentWithdrawal.nullifier,
subsetRoot: recentWithdrawal.subsetRoot
}
// requestPolicy: 'cache-and-network'
});
useEffect(() => {
if (
Array.isArray(result?.data?.subsetDatas) &&
result!.data!.subsetDatas.length === 1
) {
const { accessType, bitLength, subsetData } =
result!.data!.subsetDatas[0];
setSubsetMetadata({
accessType: accessType === '1' ? 'blocklist' : 'allowlist',
bitLength: Number(bitLength),
subsetData: Buffer.from(subsetData.slice(2), 'hex')
});
} else {
setSubsetMetadata({
accessType: 'blocklist',
bitLength: NaN,
subsetData: Buffer.alloc(0)
});
}
}, [result, setSubsetMetadata]);
return { subsetMetadata, executeSubsetDatasQuery };
}

View File

@@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';
import { useQuery } from 'urql';
import { subsetRootsAtom } from '../state/atoms';
import { useContractAddress } from '../hooks';
import {
SubsetRootsByTimestampQuery,
SubsetRootsByTimestampDocument
} from '../query';
export function useSubsetRoots() {
const [subsetRoots, setSubsetRoots] = useAtom(subsetRootsAtom);
const { contractAddress } = useContractAddress();
const [result, executeSubsetRootsQuery] =
useQuery<SubsetRootsByTimestampQuery>({
query: SubsetRootsByTimestampDocument,
variables: {
timestamp: 0,
contractAddress
},
requestPolicy: 'cache-and-network'
});
useEffect(() => {
if (Array.isArray(result?.data?.subsetRoots)) {
setSubsetRoots(result!.data!.subsetRoots);
}
}, [result, setSubsetRoots]);
return { subsetRoots, executeSubsetRootsQuery };
}

34
src/hooks/useZKeys.ts Normal file
View File

@@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { useAtom } from 'jotai';
import { wasmBytesAtom, zkeyBytesAtom } from '../state';
export function useZKeys() {
const [wasmBytes, setWasmBytes] = useAtom(wasmBytesAtom);
const [zkeyBytes, setZkeyBytes] = useAtom(zkeyBytesAtom);
useEffect(() => {
if (wasmBytes.data.length === 0) {
fetch(`/withdraw_from_subset_simple.wasm`)
.then((res) => res.arrayBuffer())
.then((buff) => {
setWasmBytes({
type: 'mem',
data: Buffer.from(buff)
});
});
}
if (zkeyBytes.data.length === 0) {
fetch(`/withdraw_from_subset_simple_final.zkey`)
.then((res) => res.arrayBuffer())
.then((buff) => {
setZkeyBytes({
type: 'mem',
data: Buffer.from(buff)
});
});
}
}, [wasmBytes, setWasmBytes, zkeyBytes, setZkeyBytes]);
return { zkeyBytes, wasmBytes };
}

59
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,59 @@
import '@rainbow-me/rainbowkit/styles.css';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { ChakraProvider, Button } from '@chakra-ui/react';
import { CloseIcon } from '@chakra-ui/icons';
import type { AppProps } from 'next/app';
import * as React from 'react';
import toast, { Toaster, ToastBar } from 'react-hot-toast';
import { WagmiConfig } from 'wagmi';
import { chains, client } from '../wagmi';
import { createClient, Provider as UrqlProvider } from 'urql';
const urqlClient = createClient({
url: 'https://api.thegraph.com/subgraphs/name/ameensol/privacy-pools'
});
function App({ Component, pageProps }: AppProps) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return (
<UrqlProvider value={urqlClient}>
<WagmiConfig client={client}>
<RainbowKitProvider
chains={chains}
modalSize="wide"
showRecentTransactions={true}
>
<ChakraProvider>
{mounted && <Component {...pageProps} />}
<Toaster position="bottom-center">
{(t) => (
<ToastBar toast={t}>
{({ icon, message }) => (
<>
{icon}
{message}
{t.type !== 'loading' && (
<Button
colorScheme="red"
variant="ghost"
size="xs"
onClick={() => toast.dismiss(t.id)}
>
<CloseIcon />
</Button>
)}
</>
)}
</ToastBar>
)}
</Toaster>
</ChakraProvider>
</RainbowKitProvider>
</WagmiConfig>
</UrqlProvider>
);
}
export default App;

378
src/pages/deposit.tsx Normal file
View File

@@ -0,0 +1,378 @@
import {
Button,
Center,
Container,
HStack,
Link,
Stack,
Text,
Tooltip,
VStack,
Select,
Spinner
} from '@chakra-ui/react';
import toast from 'react-hot-toast';
import { useAddRecentTransaction } from '@rainbow-me/rainbowkit';
import { ExternalLinkIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import NextLink from 'next/link';
import { NoteWalletConnectButton } from '../components/NoteWallet/NoteWalletConnectButton';
import { hexZeroPad, parseEther, isAddress } from 'ethers/lib/utils';
import {
useNetwork,
usePrepareContractWrite,
useContractWrite,
useWaitForTransaction,
useContractReads,
useBalance
} from 'wagmi';
import { useAtom } from 'jotai';
import { DappLayout } from '../components';
import { NoteWallet } from '../components/NoteWallet';
import { assetAtom, denominationAtom } from '../state/atoms';
import { useNote, useContractAddress, useOptions } from '../hooks';
import { privacyPoolABI } from '../constants';
import { pinchString } from '../utils';
function Page() {
const [asset, setAsset] = useAtom(assetAtom);
const [denomination, setDenomination] = useAtom(denominationAtom);
const { assetOptions, denominationOptions } = useOptions();
const { chain } = useNetwork();
const { commitment } = useNote();
const { contractAddress } = useContractAddress();
const addRecentTransaction = useAddRecentTransaction();
const {
data: poolData,
isError: isPoolDataError,
isLoading: isPoolDataLoading
} = useContractReads({
contracts: [
{
address: contractAddress,
abi: privacyPoolABI,
functionName: 'getLatestRoot'
},
{
address: contractAddress,
abi: privacyPoolABI,
functionName: 'currentLeafIndex'
}
],
enabled: typeof chain?.id === 'number' && isAddress(contractAddress)
});
const {
data: balanceData,
isLoading: isBalanceLoading,
isError: isBalanceError
} = useBalance({
address: contractAddress as `0x${string}`,
enabled: typeof chain?.id === 'number' && isAddress(contractAddress)
});
const { config, isError: isPrepareError } = usePrepareContractWrite({
address: contractAddress,
abi: privacyPoolABI,
functionName: 'deposit',
args: [hexZeroPad(commitment.toHexString(), 32)],
enabled:
Boolean(!commitment.eq(0)) &&
typeof chain?.id === 'number' &&
isAddress(contractAddress),
overrides: {
value: parseEther(denomination as string),
gasPrice: parseEther('0.00000000001')
},
onError() {
toast.error('There was an error preparing the transaction.', {
duration: 1000
});
}
});
const { data, write } = useContractWrite({
...config,
onError() {
toast.error('Failed to send transaction.');
}
});
const { isLoading } = useWaitForTransaction({
hash: data?.hash,
onSuccess() {
addRecentTransaction({
hash: data!.hash,
description: 'Deposit'
});
toast.success(
<HStack w="full" justify="space-evenly">
<VStack>
<Text color="green.600">Deposit succeeded!</Text>
<Link
w="full"
as={NextLink}
isExternal
href={`${chain?.blockExplorers?.default.url}/tx/${data?.hash}`}
>
<Text color="gray.800">
View on Etherscan <ExternalLinkIcon mx="2px" color="blue.600" />
</Text>
</Link>
</VStack>
</HStack>,
{ duration: 10000, style: { width: '100%' } }
);
}
});
const isDepositDisabled = Boolean(
isPrepareError || !write || isLoading || Boolean(commitment.eq(0))
);
return (
<DappLayout title="Deposit | Privacy 2.0">
<Container maxW="100vw" minW="216px" h="100vh">
<Container
bg="black"
px={0}
pb={4}
my={8}
borderRadius={10}
boxShadow="2xl"
>
<VStack
w="full"
borderRadius={10}
borderBottomRadius={0}
bg="white"
p={4}
>
<HStack>
<Tooltip
label={`Amount of asset to deposit. Higher denomination pools may take longer to gather large anonymity sets.`}
>
<QuestionOutlineIcon />
</Tooltip>
<Text fontSize="md">Value</Text>
</HStack>
<Container p={0}>
<HStack w="full">
<Select
size="md"
bg="gray.50"
onChange={(e) => setDenomination(e.target.value)}
defaultValue={denomination}
>
{denominationOptions.map((_denomination, i) => (
<option
value={_denomination}
key={`${_denomination}-${i}-option`}
>
{_denomination.toString()}
</option>
))}
</Select>
<Select
size="md"
bg="gray.50"
onChange={(e) => setAsset(e.target.value)}
defaultValue={asset}
>
{assetOptions.map((_asset, i) => (
<option value={_asset} key={`${_asset}-${i}-option`}>
{_asset.toString()}
</option>
))}
</Select>
</HStack>
</Container>
<HStack w="full" justify="space-between" py={2}>
<VStack>
<HStack>
<Tooltip
label={`The current number of deposits that have joined the pool. Higher is better!`}
>
<QuestionOutlineIcon />
</Tooltip>
<Text fontSize="md">Pool Size</Text>
</HStack>
<Text fontWeight="bold">
{isPoolDataLoading ? (
<Spinner />
) : isPoolDataError ? (
'--'
) : (
(
((poolData as [string, BigInt]) || [null, null])[1] || 0
).toString()
)}
</Text>
</VStack>
<VStack>
<HStack>
<Tooltip
label={`The commitment is publicly recorded in the privacy pool.`}
>
<QuestionOutlineIcon />
</Tooltip>
<Text fontSize="md">Commitment</Text>
</HStack>
<Tooltip label={hexZeroPad(commitment.toHexString(), 32)}>
<Text
fontSize="lg"
sx={{ wordBreak: 'break-word' }}
fontWeight="bold"
_hover={{ color: 'gray.500' }}
>
{pinchString(hexZeroPad(commitment.toHexString(), 32), 6)}
</Text>
</Tooltip>
</VStack>
</HStack>
<HStack w="full" justify="space-between" py={2}>
<VStack>
<HStack>
<Tooltip
label={`The current balance of the pool. Fluctuates depending on how many depositors are waiting to withdraw.`}
>
<QuestionOutlineIcon />
</Tooltip>
<Text fontSize="md">Pool Balance</Text>
</HStack>
<Text fontWeight="bold">
{isBalanceLoading ? (
<Spinner />
) : isBalanceError ? (
'--'
) : (
balanceData?.formatted
)}
{balanceData?.symbol === asset
? ` ${asset}`
: ` ${balanceData?.symbol} (${asset})`}
</Text>
</VStack>
<VStack>
<HStack>
<Tooltip
label={`The current merkle root of deposits that have joined the pool. Used in zero knowledge to prove that the commitment is deposited in the pool.`}
>
<QuestionOutlineIcon />
</Tooltip>
<Text fontSize="md">Root</Text>
</HStack>
<Tooltip
label={((poolData as [string, BigInt]) || [null, null])[0]}
>
<Text
fontSize="lg"
sx={{ wordBreak: 'break-word' }}
fontWeight="bold"
_hover={{ color: 'gray.500' }}
>
{isPoolDataLoading ? (
<Spinner />
) : isPoolDataError ? (
'--'
) : (
pinchString(
(
((poolData as [string, BigInt]) || [null, null])[0] ||
0
).toString(),
6
)
)}
</Text>
</Tooltip>
</VStack>
</HStack>
</VStack>
{asset !== 'ETH' ? (
<>
<Stack>
<Center h="100%" pl={4}>
<Button
colorScheme="pink"
size="lg"
w="full"
isDisabled={!Boolean(contractAddress)}
>
Approve
</Button>
</Center>
</Stack>
<Stack>
<Center h="100%" pr={4}>
<Button
bg="gray.200"
color="black"
fontSize="lg"
fontWeight="bold"
_hover={{
bg: 'gray.700',
color: 'white',
borderColor: 'gray.600',
transform: 'scaleX(1.01)'
}}
size="lg"
w="full"
isDisabled={!Boolean(contractAddress)}
>
Deposit
</Button>
</Center>
</Stack>
</>
) : (
<Stack>
<Center h="100%" pt={4} px={4}>
{commitment.eq(0) ? (
<Text color="white" fontSize="lg" fontWeight="bold">
Connect your Note Wallet
</Text>
) : (
<Button
bg="gray.100"
color="black"
fontSize="lg"
fontWeight="bold"
_hover={{
bg: 'white',
color: 'black',
borderColor: 'gray.600',
transform: 'scaleX(1.05)'
}}
size="lg"
w="50%"
isDisabled={isDepositDisabled}
onClick={() => {
write?.({
overrides: {
value: parseEther(denomination)
}
} as any);
}}
>
{isLoading ? <Spinner /> : 'Deposit'}
</Button>
)}
</Center>
</Stack>
)}
</Container>
</Container>
</DappLayout>
);
}
export default Page;

715
src/pages/explorer.tsx Normal file
View File

@@ -0,0 +1,715 @@
import { useState, useEffect } from 'react';
import {
Box,
Button,
Container,
Heading,
Stack,
HStack,
VStack,
Text,
Link,
Tooltip
} from '@chakra-ui/react';
import {
ExternalLinkIcon,
ViewIcon,
QuestionOutlineIcon,
RepeatIcon
} from '@chakra-ui/icons';
import NextLink from 'next/link';
import { useNetwork } from 'wagmi';
import { useAtom } from 'jotai';
import { DappLayout, AssetDenominationBar } from '../components';
import { useSubsetRoots, useExplorerData } from '../hooks';
import { recentWithdrawalAtom } from '../state';
import { growShrinkProps, pinchString } from '../utils';
function Page() {
const [isFetching, setIsFetching] = useState<boolean>(false);
const [isWithdrawalsCollapsed, setIsWithdrawalsCollapsed] =
useState<boolean>(false);
const [isInfoCollapsed, setIsInfoCollapsed] = useState<boolean>(false);
const [isIncludedDepositsCollapsed, setIsIncludedDepositsCollapsed] =
useState<boolean>(true);
const [isExcludedDepositsCollapsed, setIsExcludedDepositsCollapsed] =
useState<boolean>(true);
const [recentWithdrawal, setRecentWithdrawal] = useAtom(recentWithdrawalAtom);
const { chain } = useNetwork();
const { subsetRoots, executeSubsetRootsQuery } = useSubsetRoots();
const { accessList, includedDeposits, excludedDeposits } = useExplorerData();
return (
<DappLayout title="Stats | Privacy 2.0">
<Box pb="8rem">
<Container my={8} py={8} minW="216px" maxW="960px">
<VStack bg="gray.50" boxShadow="xl" borderRadius={8}>
<HStack w="full" bg="gray.200" borderRadius="8px 8px 0 0" p={4}>
<Text color="black" fontWeight="bold">
Choose a pool
</Text>
</HStack>
<HStack w="full" justify="center" pb={2}>
<AssetDenominationBar />
</HStack>
</VStack>
</Container>
<Container centerContent minW="216px" maxW="960px" py={15} my={5}>
<Stack
direction="row"
w="full"
justify="space-between"
align="center"
bg="gray.200"
p={4}
borderBottom="none"
borderRadius="8px 8px 0 0"
boxShadow="2xl"
>
<Heading color="black" size="sm">
Recent Withdrawals
</Heading>
<HStack>
<Text fontWeight="bold" color="blue.700" fontSize="sm">
Total: {subsetRoots?.length}
</Text>
<Button
onClick={() => {
setIsFetching(true);
executeSubsetRootsQuery();
setTimeout(() => setIsFetching(false), 7500);
}}
isDisabled={isFetching}
p={0}
>
<RepeatIcon />
</Button>
</HStack>
</Stack>
<Stack
w="full"
bg="gray.50"
borderTop="none"
borderRadius="0 0 8px 8px"
>
{subsetRoots.length === 0 ? (
<Stack align="center" p={4}>
<Text color="blue.700" fontSize="sm" fontWeight="bold">
No withdrawals detected for this pool.
</Text>
</Stack>
) : (
<>
<Stack align="center" p={4}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() =>
setIsWithdrawalsCollapsed(!isWithdrawalsCollapsed)
}
>
<Text fontWeight="normal" fontSize="xs">
{isWithdrawalsCollapsed
? 'Show Withdrawals'
: 'Hide Withdrawls'}
</Text>
</Button>
</Stack>
{!isWithdrawalsCollapsed && (
<VStack w="full" px={4} pb={4}>
<HStack justify="center" w="full">
<Text
w="25%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
textAlign="center"
>
INSPECT
</Text>
<Text
w="25%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
RECIPIENT
</Text>
<Text
w="25%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
NULLIFIER
</Text>
<Text
w="25%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
SUBSET ROOT
</Text>
</HStack>
<VStack // 3996/1247
w="full"
minH="240px"
overflowY="auto"
bg="gray.50"
py={4}
>
{subsetRoots
.slice(
subsetRoots.length < 30 ? 0 : subsetRoots.length - 30
)
.map(
(
{ recipient, subsetRoot, nullifier: _nullifier },
index
) => (
<HStack
key={`row-${_nullifier}`}
justify="center"
w="full"
borderTop={index ? 'solid 1px #BEE3F8' : 'none'}
pt={4}
>
<HStack w="25%" justify="center">
<Button
size="sm"
bg={
recentWithdrawal.nullifier !== _nullifier
? 'white'
: 'black'
}
_hover={
recentWithdrawal.nullifier !== _nullifier
? {
bg: 'gray.200',
color: 'white',
transform: 'scaleX(1.01)'
}
: {}
}
border="2px solid black"
color="white"
onClick={() => {
setRecentWithdrawal({
recipient,
subsetRoot,
nullifier: _nullifier
});
}}
>
<ViewIcon
color={
recentWithdrawal.nullifier !== _nullifier
? 'black'
: 'white'
}
/>
</Button>
</HStack>
<HStack w="25%" justify="flex-start">
<Link
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${recipient}`}
isExternal
>
<Text
key={`recipient-${_nullifier}`}
color="blue.700"
fontSize="sm"
textAlign="left"
wordBreak="break-all"
{...growShrinkProps}
>
{pinchString(recipient.toString(), 5)}{' '}
<ExternalLinkIcon />
</Text>
</Link>
</HStack>
<HStack w="25%" justify="flex-start">
<Text>{pinchString(_nullifier, 6)}</Text>
</HStack>
<HStack w="25%" justify="flex-start">
<Text>{pinchString(subsetRoot, 6)}</Text>
</HStack>
</HStack>
)
)}
</VStack>
<HStack justify="center" w="full">
<Box w="25%" />
<HStack w="25%" justify="flex-start">
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
fontWeight="bold"
fontSize="xs"
>
ADDRESS
</Text>
</HStack>
<HStack w="25%" justify="flex-start">
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
fontWeight="bold"
fontSize="xs"
>
BYTES32
</Text>
</HStack>
<HStack w="25%" justify="flex-start">
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
fontWeight="bold"
fontSize="xs"
>
BYTES32
</Text>
</HStack>
</HStack>
</VStack>
)}
</>
)}
</Stack>
</Container>
{accessList.length > 0 && (
<Container centerContent minW="216px" maxW="960px" mb={40}>
<Stack w="full">
<Stack
w="full"
direction="row"
justify="space-between"
align="center"
bg="gray.100"
mt={4}
p={4}
borderBottom="none"
borderRadius="8px 8px 0 0"
>
<Heading color="blue.700" size="sm">
Subset Explorer
</Heading>
<Text fontWeight="bold" color="blue.700" fontSize="sm">
Total: {accessList.length}
</Text>
</Stack>
</Stack>
<Stack
w="full"
bg="gray.50"
border="solid 1px #BEE3F8"
borderTop="none"
borderRadius="0 0 8px 8px"
p={4}
>
<Stack align="center" p={4}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() => setIsInfoCollapsed(!isInfoCollapsed)}
>
<Text fontWeight="normal" fontSize="xs">
{isInfoCollapsed ? 'Show Info' : 'Hide Info'}
</Text>
</Button>
</Stack>
{!isInfoCollapsed && (
<>
<HStack justify="center">
<HStack>
<Tooltip label="The subset data of an access list is valid if the subset root can be computed faithfully. If the root does not match, then the subset data is corrupted.">
<QuestionOutlineIcon />
</Tooltip>
<Text>Verified</Text>
</HStack>
{recentWithdrawal.subsetRoot ===
accessList.root.toHexString() ? (
<Text textAlign="center" color="green.600">
The subset is valid!
</Text>
) : (
<Text textAlign="center" color="red.600">
The subset is invalid!
</Text>
)}
</HStack>
<Stack direction={['column', 'row']}>
<VStack w={['full', '50%']} wordBreak="break-word">
<HStack>
<Tooltip label="The expected subset root was verified in the zero knowledge proof during the withdrawal transaction. This is the real subset root used to withdraw.">
<QuestionOutlineIcon />
</Tooltip>
<Text>Expected Subset Root</Text>
</HStack>
<HStack
bg="gray.50"
borderRadius={6}
w="full"
p={2}
flexGrow={1}
>
<Text fontWeight="bold" fontSize="sm">
{recentWithdrawal.subsetRoot}
</Text>
</HStack>
</VStack>
<VStack w={['full', '50%']} wordBreak="break-word">
<HStack>
<Tooltip label="The computed subset root is calculated using off-chain subset data. If the computed subset root matches the expected root, then the subset data is verified.">
<QuestionOutlineIcon />
</Tooltip>
<Text>Computed Subset Root</Text>
</HStack>
<HStack
bg="gray.50"
borderRadius={6}
flexGrow={1}
w="full"
p={2}
>
<Text fontWeight="bold" fontSize="sm">
{accessList.root.toHexString()}
</Text>
</HStack>
</VStack>
</Stack>
<Stack direction={['column', 'row']}>
<VStack w={['full', '50%']} wordBreak="break-word">
<HStack>
<Tooltip label="The recipient received the asset during the withdrawal. The funds could have come from any of the deposits in the included deposits, but it cannot have come from the excluded deposits!">
<QuestionOutlineIcon />
</Tooltip>
<Text>Recipient</Text>
</HStack>
<HStack
bg="gray.50"
borderRadius={6}
w="full"
p={2}
flexGrow="1"
>
<Text fontWeight="bold" fontSize="sm">
{recentWithdrawal.recipient}
</Text>
</HStack>
</VStack>
<VStack w={['full', '50%']} wordBreak="break-word">
<HStack>
<Tooltip label="The nullifier is calculated from the index of the commitment in the tree and the secret. The nullifier prevents double spends and is unique to each withdrawal.">
<QuestionOutlineIcon />
</Tooltip>
<Text>Nullifier</Text>
</HStack>
<HStack
bg="gray.50"
borderRadius={6}
flexGrow={1}
w="full"
p={2}
>
<Text fontWeight="bold" fontSize="sm">
{recentWithdrawal.nullifier}
</Text>
</HStack>
</VStack>
</Stack>
</>
)}
{recentWithdrawal.subsetRoot ===
accessList.root.toHexString() && (
<>
<HStack justify="space-between" align="center" pt={4}>
<HStack>
<Tooltip label="The withdrawal is cryptographically verified to have originated from one of these deposits.">
<QuestionOutlineIcon />
</Tooltip>
<Heading color="blue.700" size="sm">
Included Deposits
</Heading>
</HStack>
<Text fontWeight="bold" color="blue.700" fontSize="sm">
Total: {includedDeposits.length}
</Text>
</HStack>
<Stack align="center" p={4}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() =>
setIsIncludedDepositsCollapsed(
!isIncludedDepositsCollapsed
)
}
>
<Text fontWeight="normal" fontSize="xs">
{isIncludedDepositsCollapsed
? 'Show Included Deposits'
: 'Hide Included Deposits'}
</Text>
</Button>
</Stack>
{!isIncludedDepositsCollapsed && (
<>
<HStack justify="center" w="full">
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
textAlign="center"
>
LEAF INDEX
</Text>
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
SENDER
</Text>
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
COMMITMENT
</Text>
</HStack>
<VStack
w="full"
minH="360px"
overflowY="auto"
bg="gray.50"
py={4}
gap={1}
>
{includedDeposits
.slice(
includedDeposits.length < 30
? 0
: includedDeposits.length - 30
)
.map(({ commitment, sender, leafIndex }, index) => (
<HStack
key={`included-${leafIndex}`}
justify="center"
w="full"
borderTop={index ? 'solid 1px #BEE3F8' : 'none'}
pt={4}
>
<HStack w="33%" justify="center">
<Text>{leafIndex.toString()}</Text>
</HStack>
<HStack w="33%" justify="flex-start">
<Link
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${sender}`}
isExternal
>
<Text
key={`sender-${leafIndex}`}
color="blue.700"
fontSize="sm"
textAlign="left"
wordBreak="break-all"
{...growShrinkProps}
>
{pinchString(sender.toString(), 5)}{' '}
<ExternalLinkIcon />
</Text>
</Link>
</HStack>
<HStack w="33%" justify="flex-start">
<Text>{pinchString(commitment, 6)}</Text>
</HStack>
</HStack>
))}
</VStack>
</>
)}
<HStack justify="space-between" align="center" pt={4}>
<HStack>
<Tooltip label="The withdrawal is cryptographically verified to NOT have originated from one of these deposits. These are the bad guys!">
<QuestionOutlineIcon />
</Tooltip>
<Heading color="blue.700" size="sm">
Excluded Deposits
</Heading>
</HStack>
<Text fontWeight="bold" color="blue.700" fontSize="sm">
Total: {excludedDeposits.length}
</Text>
</HStack>
<Stack align="center" p={4}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() =>
setIsExcludedDepositsCollapsed(
!isExcludedDepositsCollapsed
)
}
>
<Text fontWeight="normal" fontSize="xs">
{isExcludedDepositsCollapsed
? 'Show Excluded Deposits'
: 'Hide Excluded Deposits'}
</Text>
</Button>
</Stack>
{!isExcludedDepositsCollapsed && (
<>
<HStack justify="center" w="full">
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
textAlign="center"
>
LEAF INDEX
</Text>
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
SENDER
</Text>
<Text
w="33%"
color="blue.600"
borderRadius={8}
fontWeight="bold"
fontSize="xs"
>
COMMITMENT
</Text>
</HStack>
<VStack
w="full"
maxH="360px"
overflowY="auto"
bg="gray.50"
py={4}
gap={1}
>
{excludedDeposits
.slice(
excludedDeposits.length < 30
? 0
: excludedDeposits.length - 30
)
.map(({ commitment, sender, leafIndex }, index) => (
<HStack
key={`included-${leafIndex}`}
justify="center"
w="full"
borderTop={index ? 'solid 1px #BEE3F8' : 'none'}
pt={4}
>
<HStack w="33%" justify="center">
<Text>{leafIndex.toString()}</Text>
</HStack>
<HStack w="33%" justify="flex-start">
<Link
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${sender}`}
isExternal
>
<Text
key={`sender-${leafIndex}`}
color="blue.700"
fontSize="sm"
textAlign="left"
wordBreak="break-all"
{...growShrinkProps}
>
{pinchString(sender.toString(), 5)}{' '}
<ExternalLinkIcon />
</Text>
</Link>
</HStack>
<HStack w="33%" justify="flex-start">
<Text>{pinchString(commitment, 6)}</Text>
</HStack>
</HStack>
))}
</VStack>
</>
)}
</>
)}
</Stack>
</Container>
)}
</Box>
</DappLayout>
);
}
export default Page;

32
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { Box } from '@chakra-ui/react';
import HeroSection from '../components/LandingPage/HeroSection';
import InfoSection from '../components/LandingPage/InfoSection';
import NavBar from '../components/LandingPage/NavBar';
import Footer from '../components/LandingPage/Footer';
/**
*
* css-gradient: https://cssgradient.io/
*/
const Page = () => {
return (
<Box w="100vw" h="100vh" mt="0" m="auto">
<Box
maxW="100vw"
px={{ base: 10, md: 40 }}
alignItems="center"
// bgGradient="linear-gradient(220deg, rgba(172,186,246,0.68) 17%, rgba(0,212,255,0.6) 77%, rgba(139,231,224,0.8) 97%)"
// bgGradient="linear-gradient(45deg, #b8faf4 0%, #a8bdfc 25%, #c5b0f6 50%, #ffe5f9 75%, #f0ccc3 100%)"
bgGradient="linear-gradient(45deg, #b8faf480 0%, #a8bdfc80 25%, #c5b0f680 50%, #ffe5f980 75%, #f0ccc380 100%)"
>
<NavBar />
<HeroSection />
<InfoSection />
</Box>
<Footer />
</Box>
);
};
export default Page;

515
src/pages/stats.tsx Normal file
View File

@@ -0,0 +1,515 @@
import { useState } from 'react';
import {
Button,
Container,
Heading,
Stack,
HStack,
Text,
Table,
TableContainer,
Thead,
Td,
Tr,
Tbody,
TableCaption,
Th,
Tfoot,
Link
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import NextLink from 'next/link';
import { useNetwork } from 'wagmi';
import { hexZeroPad } from 'ethers/lib/utils';
import { DappLayout, PoolExplorer } from '../components';
import { useCommitments, useSubsetRoots, useContractAddress } from '../hooks';
import { growShrinkProps, pinchString } from '../utils';
function Page() {
const [isDepositsCollapsed, setIsDepositsCollapsed] = useState<boolean>(true);
const [isWithdrawalsCollapsed, setIsWithdrawalsCollapsed] =
useState<boolean>(true);
const { chain } = useNetwork();
const { contractAddress } = useContractAddress();
const { commitments } = useCommitments();
const { subsetRoots } = useSubsetRoots();
return (
<DappLayout title="Stats | Privacy 2.0">
<PoolExplorer />
{Array.isArray(commitments) && commitments.length > 0 && (
<Container centerContent minW="216px" maxW="960px">
<Stack
w="full"
direction="row"
justify="space-between"
align="center"
position="sticky"
zIndex="2"
top={0}
bg="gray.300"
mt={4}
p={4}
border="solid 1px #BEE3F8"
borderBottom="none"
borderRadius="8px 8px 0 0"
>
<Heading color="black" size="sm">
Deposits
</Heading>
<Text fontWeight="bold" color="black" fontSize="sm">
Total: {commitments?.length}
</Text>
</Stack>
<Stack
w="full"
bg="gray.50"
border="solid 1px #BEE3F8"
borderTop="none"
borderRadius="0 0 8px 8px"
>
<Stack align="center" py={2} px={2}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() => setIsDepositsCollapsed(!isDepositsCollapsed)}
>
Show Deposits
</Button>
</Stack>
{!isDepositsCollapsed && (
<TableContainer
whiteSpace="unset"
maxH="42vh"
overflowY="auto"
p={2}
w="full"
>
<Table variant="simple" size="md" colorScheme="blue">
<TableCaption>
Deposits list for {contractAddress}
</TableCaption>
<Thead>
<Tr>
<Th>
<Text color="blue.600">#</Text>
</Th>
<Th>
<HStack>
<Text color="blue.600">Sender</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Commitment</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
</Tr>
</Thead>
<Tbody>
{commitments
.slice(
commitments.length < 30 ? 0 : commitments.length - 30
)
.map(({ commitment, leafIndex, sender }) => (
<Tr key={`commitments-row-${leafIndex}`}>
<Td>{leafIndex}</Td>
<Td>
<Link
w="full"
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${sender}`}
isExternal
>
<Text
key={`sender-${leafIndex}`}
color="blue.700"
fontSize="sm"
textAlign="left"
wordBreak="break-all"
{...growShrinkProps}
>
{pinchString(sender.toString(), 12)}{' '}
<ExternalLinkIcon />
</Text>
</Link>
</Td>
<Td>
<Text wordBreak="break-all">
{pinchString(hexZeroPad(commitment, 32), 16)}
</Text>
</Td>
</Tr>
))}
</Tbody>
<Tfoot>
<Tr>
<Th>
<Text color="blue.600">#</Text>
</Th>
<Th>
<HStack>
<Text color="blue.600">Sender</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Commitment</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
</Tr>
</Tfoot>
</Table>
</TableContainer>
)}
</Stack>
</Container>
)}
{Array.isArray(subsetRoots) && subsetRoots.length > 0 && (
<Container centerContent minW="216px" maxW="960px" mb={40}>
<Stack w="full">
<Stack
w="full"
direction="row"
justify="space-between"
align="center"
position="sticky"
zIndex="2"
top={0}
bg="gray.200"
mt={4}
p={4}
border="solid 1px #BEE3F8"
borderBottom="none"
borderRadius="8px 8px 0 0"
>
<Heading color="black" size="sm">
Withdrawals
</Heading>
<Text fontWeight="bold" color="black" fontSize="sm">
Total: {subsetRoots?.length}
</Text>
</Stack>
</Stack>
<Stack
w="full"
bg="gray.50"
border="solid 1px #BEE3F8"
borderTop="none"
borderRadius="0 0 8px 8px"
>
<Stack align="center" p={2}>
<Button
w="full"
size="sm"
variant="outline"
onClick={() =>
setIsWithdrawalsCollapsed(!isWithdrawalsCollapsed)
}
>
{isWithdrawalsCollapsed
? 'Show Withdrawals'
: 'Hide Withdrawls'}
</Button>
</Stack>
{!isWithdrawalsCollapsed && (
<TableContainer
whiteSpace="unset"
maxH="42vh"
overflowY="auto"
p={2}
>
<Table variant="simple" size="md" colorScheme="blue">
<TableCaption>
Withdrawals list for {contractAddress}
</TableCaption>
<Thead>
<Tr>
<Th>
<HStack>
<Text color="blue.600">Recipient</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Nullifier</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Relayer</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Sender</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Subset Root</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
</Tr>
</Thead>
<Tbody>
{subsetRoots
.slice(
subsetRoots.length < 30 ? 0 : subsetRoots.length - 30
)
.map(
({
recipient,
relayer,
sender,
subsetRoot,
nullifier
}) => (
<Tr key={`row-${nullifier}`}>
<Td w="20%" wordBreak="break-word">
<Link
w="full"
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${recipient}`}
isExternal
>
<Text
key={`${nullifier}-${recipient}`}
color="blue.700"
fontSize="sm"
textAlign="left"
{...growShrinkProps}
>
{pinchString(recipient, 6)}{' '}
<ExternalLinkIcon />
</Text>
</Link>
</Td>
<Td w="20%" wordBreak="break-word">
{pinchString(nullifier, 10)}
</Td>
<Td w="20%" wordBreak="break-word">
<Link
w="full"
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${relayer}`}
isExternal
>
<Text
key={`${nullifier}-${relayer}`}
color="blue.700"
fontSize="sm"
textAlign="left"
{...growShrinkProps}
>
{pinchString(relayer, 6)} <ExternalLinkIcon />
</Text>
</Link>
</Td>
<Td w="20%" wordBreak="break-word">
<Link
w="full"
as={NextLink}
{...growShrinkProps}
href={`${chain?.blockExplorers?.default.url}/address/${sender}`}
isExternal
>
<Text
key={`${nullifier}-${sender}`}
color="blue.700"
fontSize="sm"
textAlign="left"
{...growShrinkProps}
>
{pinchString(sender, 6)} <ExternalLinkIcon />
</Text>
</Link>
</Td>
<Td w="20%" wordBreak="break-word">
{pinchString(subsetRoot, 10)}
</Td>
</Tr>
)
)}
</Tbody>
<Tfoot>
<Tr>
<Th>
<HStack>
<Text color="blue.600">Recipient</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Nullifier</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Relayer</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Sender</Text>
<Text
color="green.700"
bg="teal.100"
borderRadius={8}
p={1}
>
address
</Text>
</HStack>
</Th>
<Th>
<HStack>
<Text color="blue.600">Subset Root</Text>
<Text
color="purple.700"
bg="blue.100"
borderRadius={8}
p={1}
>
bytes32
</Text>
</HStack>
</Th>
</Tr>
</Tfoot>
</Table>
</TableContainer>
)}
</Stack>
</Container>
)}
</DappLayout>
);
}
export default Page;

1185
src/pages/withdraw.tsx Normal file

File diff suppressed because it is too large Load Diff

18
src/query/commitments.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Commitment } from '../state/atoms';
export interface CommitmentsQuery {
commitments: Commitment[];
}
export const CommitmentsQueryDocument = /* GraphQL */ `
query Commitments($lastLeafIndex: Int, $contractAddress: String!) {
commitments(
orderBy: leafIndex
where: { contractAddress: $contractAddress, leafIndex_gt: $lastLeafIndex }
) {
leafIndex
commitment
sender
}
}
`;

15
src/query/index.ts Normal file
View File

@@ -0,0 +1,15 @@
export { CommitmentsQueryDocument, type CommitmentsQuery } from './commitments';
export {
SubsetRootsByRelayerDocument,
SubsetRootsBySenderDocument,
SubsetRootsByTimestampDocument,
type SubsetRootsByRelayerQuery,
type SubsetRootsBySenderQuery,
type SubsetRootsByTimestampQuery
} from './subsetRoots';
export {
SubsetDataByNullifierQueryDocument,
type SubsetDataByNullifierQuery
} from './subsetDatas';

27
src/query/subsetDatas.ts Normal file
View File

@@ -0,0 +1,27 @@
export interface SubsetDataByNullifierQuery {
subsetDatas: {
accessType: string;
bitLength: string;
subsetData: string;
}[];
}
export const SubsetDataByNullifierQueryDocument = /* GraphQL */ `
query SubsetDataByNullifier(
$contractAddress: Bytes!
$subsetRoot: Bytes!
$nullifier: Bytes!
) {
subsetDatas(
where: {
contractAddress: $contractAddress
subsetRoot: $subsetRoot
nullifier: $nullifier
}
) {
accessType
bitLength
subsetData
}
}
`;

82
src/query/subsetRoots.ts Normal file
View File

@@ -0,0 +1,82 @@
import { SubsetRoot } from '../state/atoms';
export interface SubsetRootsByTimestampQuery {
subsetRoots: SubsetRoot[];
}
export const SubsetRootsByTimestampDocument = /* GraphQL */ `
query SubsetRootsByTimestamp($timestamp: BigInt, $contractAddress: Bytes!) {
subsetRoots(
orderBy: timestamp
where: { contractAddress: $contractAddress, timestamp_gt: $timestamp }
) {
subsetRoot
relayer
recipient
nullifier
sender
}
}
`;
export interface SubsetRootsByRelayerQuery {
subsetRoots: {
subsetRoot: string;
recipient: string;
nullifier: string;
sender: string;
};
}
export const SubsetRootsByRelayerDocument = /* GraphQL */ `
query SubsetRootsByRelayer(
$timestamp: BigInt
$contractAddress: Bytes!
$relayer: Bytes!
) {
subsetRoots(
orderBy: timestamp
where: {
contractAddress: $contractAddress
timestamp_gt: $timestamp
relayer: $relayer
}
) {
subsetRoot
recipient
nullifier
sender
}
}
`;
export interface SubsetRootsBySenderQuery {
subsetRoots: {
subsetRoot: string;
relayer: string;
recipient: string;
nullifier: string;
};
}
export const SubsetRootsBySenderDocument = /* GraphQL */ `
query SubsetRootsBySender(
$timestamp: BigInt
$contractAddress: Bytes!
$sender: Bytes!
) {
subsetRoots(
orderBy: timestamp
where: {
contractAddress: $contractAddress
timestamp_gt: $timestamp
sender: $sender
}
) {
subsetRoot
recipient
nullifier
relayer
}
}
`;

195
src/state/atoms.ts Normal file
View File

@@ -0,0 +1,195 @@
import { BigNumber } from 'ethers';
import { hexZeroPad } from 'ethers/lib/utils';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { AccessList, MerkleTree, BytesData, SubsetData } from 'pools-ts';
export type EncryptedJson = {
address?: string;
crypto?: {
cipher: string;
cipherparams: {
iv: string;
};
ciphertext: string;
};
kdf?: string;
kdfparams?: {
dklen: number;
n: number;
p: number;
r: number;
salt: string;
};
mac?: string;
id?: string;
version?: number;
'x-ethers'?: {
client: string;
gethFilename: string;
locale: string;
mnemonicCiphertext: string;
mnemonicCounter: string;
path: string;
version: string;
};
};
export type Commitment = {
commitment: string;
leafIndex: string;
sender: string;
};
export type SubsetRoot = {
subsetRoot: string;
relayer: string;
recipient: string;
nullifier: string;
sender: string;
};
export type Note = {
commitment: BigNumber;
secret: BigNumber;
index: number;
};
export type Proof = {
pi_a: BigNumber[];
pi_b: BigNumber[][];
pi_c: BigNumber[];
};
export type SolidityInput = {
flatProof: string[];
root: string;
subsetRoot: string;
nullifier: string;
recipient: string;
relayer: string;
fee: string;
};
export type ZKProofMetadata = {
contractAddress: string;
asset: string;
denomination: string;
chainId: number;
accessType: string;
bitLength: number;
subsetData: SubsetData;
bytesData: BytesData;
};
export type ZKProof = {
proof: Proof;
publicSignals: BigNumber[];
solidityInput: SolidityInput;
metadata: ZKProofMetadata;
};
export type RecentWithdrawal = {
recipient: string;
nullifier: string;
subsetRoot: string;
relayer?: string;
denomination?: string;
fee?: string;
};
export type ZKeyOrWasm = {
type: 'mem';
data: Buffer;
};
type Stage = 'Connect' | 'Unlock' | 'Manage' | 'Create';
// note wallet
export const DefaultNote: Note = {
index: 0,
commitment: BigNumber.from(0),
secret: BigNumber.from(0)
};
export const stageAtom = atom<Stage>('Connect');
export const mnemonicAtom = atom<string>('');
export const noteAtom = atom<Note>(DefaultNote);
export const encryptedJsonAtom = atomWithStorage<EncryptedJson>(
'encryptedJson',
{}
);
export const activeIndexAtom = atomWithStorage<number>('activeIndex', 0);
export const downloadUrlAtom = atom<string>('');
// pool explorer
export const assetAtom = atom<string>('ETH');
export const denominationAtom = atom<string>('0.001');
// withdraw form
export const nullifierAtom = atom<BigNumber>(BigNumber.from(0));
export const leafIndexAtom = atom<number>(NaN);
export const recipientAtom = atom<string>('');
export const relayerAtom = atom<string>(
'0x000000000000000000000000000000000000dead'
);
export const feeAtom = atom<string>('0');
export const zkProofAtom = atom<ZKProof | null>(null);
// withdrawals
export const subsetRootsAtom = atom<SubsetRoot[]>([]);
export const spentNullifiersAtom = atom<Record<string, boolean>>((get) => {
const nullifiers = get(subsetRootsAtom).map(
(subsetRoot) => subsetRoot.nullifier
);
const spentNullifiers: Record<string, boolean> = {};
for (const nullifier of nullifiers) {
spentNullifiers[hexZeroPad(nullifier.toString(), 32)] = true;
}
return spentNullifiers;
});
// deposits
export const commitmentsAtom = atom<Commitment[]>([]);
export const depositsTreeAtom = atom<MerkleTree>(
new MerkleTree({ leaves: [] })
);
export const depositsRootAtom = atom<BigNumber>(
(get) => get(depositsTreeAtom).root
);
// withdrawal subsets
export const accessListAtom = atom<AccessList>(
new AccessList({ accessType: 'blocklist' })
);
export const subsetRootAtom = atom<BigNumber>(
(get) => get(accessListAtom).root
);
// explorer
export type SubsetMetaData = {
accessType: string;
bitLength: number;
subsetData: Buffer;
};
export const subsetMetadataAtom = atom<SubsetMetaData>({
accessType: '',
bitLength: NaN,
subsetData: Buffer.alloc(0)
});
export const recentWithdrawalAtom = atom<RecentWithdrawal>({
recipient: '',
nullifier: '',
subsetRoot: ''
});
// zkeys
export const zkeyBytesAtom = atom<ZKeyOrWasm>({
type: 'mem',
data: Buffer.alloc(0)
});
export const wasmBytesAtom = atom<ZKeyOrWasm>({
type: 'mem',
data: Buffer.alloc(0)
});

1
src/state/common.ts Normal file
View File

@@ -0,0 +1 @@
export {};

1
src/state/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './atoms';

27
src/utils.ts Normal file
View File

@@ -0,0 +1,27 @@
export const growShrinkProps = {
_hover: {
transform: 'scale(1.025)'
},
_active: {
transform: 'scale(0.95)'
},
transition: '0.125s ease'
};
export const pinchString = (
s: string,
n: number | [number, number]
): string => {
if (Array.isArray(n)) {
return `${s.slice(0, n[0])}...${s.slice(s.length - n[1])}`;
}
return `${s.slice(0, n)}...${s.slice(s.length - n)}`;
};
const decimalNumber = new RegExp(
`(^[0-9]{1,60}.[0-9]{1,18}$|^[.]{1}[0-9]{1,18}$|^[0-9]{1,60}[.]{1}$)`
);
export const isDecimalNumber = (n: string): Boolean => {
if (!n) return false;
return decimalNumber.test(n);
};

44
src/wagmi.ts Normal file
View File

@@ -0,0 +1,44 @@
import { connectorsForWallets } from '@rainbow-me/rainbowkit';
import {
metaMaskWallet,
coinbaseWallet,
walletConnectWallet,
rainbowWallet,
ledgerWallet,
injectedWallet
} from '@rainbow-me/rainbowkit/wallets';
import { configureChains, createClient } from 'wagmi';
import { optimismGoerli } from 'wagmi/chains';
import { publicProvider } from 'wagmi/providers/public';
import { alchemyProvider } from 'wagmi/providers/alchemy';
const { chains, provider, webSocketProvider } = configureChains(
[optimismGoerli],
[
alchemyProvider({ apiKey: 'z9BSR-8Q26RfEqwU4D3_BJT_eOSI80yf' }),
publicProvider()
]
);
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
injectedWallet({ chains }),
metaMaskWallet({ chains }),
rainbowWallet({ chains }),
coinbaseWallet({ appName: 'privacy-pools', chains }),
ledgerWallet({ chains }),
walletConnectWallet({ chains })
]
}
]);
export const client = createClient({
autoConnect: true,
connectors,
provider,
webSocketProvider
});
export { chains };

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"paths": {
"@/components/*": ["components/*"],
"@/api/*": ["pages/api/*"],
"@/styles/*": ["styles/*"],
"@/assets/*": ["public/assets/*"]
}
}

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long