mirror of
https://github.com/ameensol/pools-ui.git
synced 2026-01-08 21:08:01 -05:00
init
This commit is contained in:
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal 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
14
.graphclientrc.yml
Normal 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
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
strict-peer-dependencies = false
|
||||
legacy-peer-deps = true
|
||||
4
.prettierrc.yaml
Normal file
4
.prettierrc.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
tabWidth: 2
|
||||
semi: true
|
||||
trailingComma: none
|
||||
singleQuote: true
|
||||
32
README.md
Normal file
32
README.md
Normal 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
3
index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "zxcvbn"
|
||||
declare module "privacy-pools"
|
||||
declare module "snarkjs"
|
||||
57
package.json
Normal file
57
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/eth_add1_placeholder.png
Normal file
BIN
public/eth_add1_placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
public/eth_add2_placeholder.png
Normal file
BIN
public/eth_add2_placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
28
public/eth_pools_logo_bw.svg
Normal file
28
public/eth_pools_logo_bw.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 508 KiB |
BIN
public/withdraw_from_subset_simple.wasm
Normal file
BIN
public/withdraw_from_subset_simple.wasm
Normal file
Binary file not shown.
BIN
public/withdraw_from_subset_simple_final.zkey
Normal file
BIN
public/withdraw_from_subset_simple_final.zkey
Normal file
Binary file not shown.
59
src/components/AssetDenominationBar.tsx
Normal file
59
src/components/AssetDenominationBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
src/components/CenteredPage.tsx
Normal file
32
src/components/CenteredPage.tsx
Normal 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;
|
||||
50
src/components/DappFooter.tsx
Normal file
50
src/components/DappFooter.tsx
Normal 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;
|
||||
50
src/components/DappLayout.tsx
Normal file
50
src/components/DappLayout.tsx
Normal 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;
|
||||
167
src/components/HeaderNavbar.tsx
Normal file
167
src/components/HeaderNavbar.tsx
Normal 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;
|
||||
85
src/components/HomeLayout.tsx
Normal file
85
src/components/HomeLayout.tsx
Normal 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;
|
||||
1
src/components/KeyRing/index.ts
Normal file
1
src/components/KeyRing/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
155
src/components/LandingPage/Footer.tsx
Normal file
155
src/components/LandingPage/Footer.tsx
Normal 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">
|
||||
"The best known cryptographic problem is that of privacy:
|
||||
preventing the unauthorized extraction of information from
|
||||
communications over an insecure channel."
|
||||
</Text>
|
||||
|
||||
<Text fontWeight="bold" textAlign="right" mt={4}>
|
||||
- Diffie and Hellman, "New Directions in Cryptography"
|
||||
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;
|
||||
173
src/components/LandingPage/HeroSection.tsx
Normal file
173
src/components/LandingPage/HeroSection.tsx
Normal 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;
|
||||
213
src/components/LandingPage/InfoSection.tsx
Normal file
213
src/components/LandingPage/InfoSection.tsx
Normal 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;
|
||||
136
src/components/LandingPage/NavBar.tsx
Normal file
136
src/components/LandingPage/NavBar.tsx
Normal 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;
|
||||
88
src/components/NoteWallet/Connect.tsx
Normal file
88
src/components/NoteWallet/Connect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
437
src/components/NoteWallet/Create.tsx
Normal file
437
src/components/NoteWallet/Create.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
289
src/components/NoteWallet/Manage.tsx
Normal file
289
src/components/NoteWallet/Manage.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
34
src/components/NoteWallet/NoteWallet.tsx
Normal file
34
src/components/NoteWallet/NoteWallet.tsx
Normal 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;
|
||||
141
src/components/NoteWallet/NoteWalletConnectButton.tsx
Normal file
141
src/components/NoteWallet/NoteWalletConnectButton.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
src/components/NoteWallet/PasswordInput.tsx
Normal file
41
src/components/NoteWallet/PasswordInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
278
src/components/NoteWallet/Unlock.tsx
Normal file
278
src/components/NoteWallet/Unlock.tsx
Normal 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't Have A Note Wallet? </Text>
|
||||
<Link color="blue.500" onClick={() => setStage('Create')} ml={1}>
|
||||
Create One
|
||||
</Link>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Center>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
2
src/components/NoteWallet/index.ts
Normal file
2
src/components/NoteWallet/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { NoteWallet } from './NoteWallet';
|
||||
export { NoteWalletConnectButton } from './NoteWalletConnectButton';
|
||||
184
src/components/PoolExplorer.tsx
Normal file
184
src/components/PoolExplorer.tsx
Normal 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;
|
||||
224
src/components/SubsetMaker/SubsetMaker.tsx
Normal file
224
src/components/SubsetMaker/SubsetMaker.tsx
Normal 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;
|
||||
55
src/components/SubsetMaker/SubsetMakerButton.tsx
Normal file
55
src/components/SubsetMaker/SubsetMakerButton.tsx
Normal 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;
|
||||
1
src/components/SubsetMaker/index.ts
Normal file
1
src/components/SubsetMaker/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SubsetMakerButton } from './SubsetMakerButton';
|
||||
8
src/components/index.ts
Normal file
8
src/components/index.ts
Normal 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
44
src/constants/index.ts
Normal 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'
|
||||
}
|
||||
};
|
||||
368
src/constants/privacyPoolAbi.ts
Normal file
368
src/constants/privacyPoolAbi.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
171
src/constants/subsetRegistryAbi.ts
Normal file
171
src/constants/subsetRegistryAbi.ts
Normal 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
12
src/hooks/index.ts
Normal 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';
|
||||
49
src/hooks/useAccessList.ts
Normal file
49
src/hooks/useAccessList.ts
Normal 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 };
|
||||
}
|
||||
28
src/hooks/useCommitments.ts
Normal file
28
src/hooks/useCommitments.ts
Normal 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 };
|
||||
}
|
||||
18
src/hooks/useContractAddress.ts
Normal file
18
src/hooks/useContractAddress.ts
Normal 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
17
src/hooks/useDebounce.ts
Normal 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;
|
||||
38
src/hooks/useDepositsTree.ts
Normal file
38
src/hooks/useDepositsTree.ts
Normal 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 };
|
||||
}
|
||||
37
src/hooks/useExistingCommitments.ts
Normal file
37
src/hooks/useExistingCommitments.ts
Normal 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 };
|
||||
}
|
||||
69
src/hooks/useExplorerData.ts
Normal file
69
src/hooks/useExplorerData.ts
Normal 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
12
src/hooks/useNote.ts
Normal 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
32
src/hooks/useOptions.ts
Normal 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 };
|
||||
}
|
||||
50
src/hooks/useSubsetDataByNullifier.ts
Normal file
50
src/hooks/useSubsetDataByNullifier.ts
Normal 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 };
|
||||
}
|
||||
32
src/hooks/useSubsetRoots.ts
Normal file
32
src/hooks/useSubsetRoots.ts
Normal 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
34
src/hooks/useZKeys.ts
Normal 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
59
src/pages/_app.tsx
Normal 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
378
src/pages/deposit.tsx
Normal 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
715
src/pages/explorer.tsx
Normal 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
32
src/pages/index.tsx
Normal 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
515
src/pages/stats.tsx
Normal 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
1185
src/pages/withdraw.tsx
Normal file
File diff suppressed because it is too large
Load Diff
18
src/query/commitments.ts
Normal file
18
src/query/commitments.ts
Normal 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
15
src/query/index.ts
Normal 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
27
src/query/subsetDatas.ts
Normal 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
82
src/query/subsetRoots.ts
Normal 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
195
src/state/atoms.ts
Normal 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
1
src/state/common.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
src/state/index.ts
Normal file
1
src/state/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './atoms';
|
||||
27
src/utils.ts
Normal file
27
src/utils.ts
Normal 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
44
src/wagmi.ts
Normal 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
26
tsconfig.json
Normal 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
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user