├── contracts └── deployments │ ├── .gitkeep │ ├── sepolia.json │ └── mainnet.json ├── .github ├── CODEOWNERS ├── codecov.yml ├── dependabot.yml ├── workflows │ ├── ui_mainnet.yml │ └── ui_sepolia.yml └── pull_request_template.md ├── ui ├── next.config.js ├── public │ ├── favicon.ico │ ├── social.jpg │ ├── passport │ │ ├── art3.png │ │ ├── art4.png │ │ ├── icon-plain.svg │ │ ├── ballot.svg │ │ ├── contactless.svg │ │ ├── discord.svg │ │ ├── logo-plain.svg │ │ ├── art.svg │ │ └── art2.svg │ ├── fonts │ │ ├── Bossa-Bold.woff │ │ ├── Bossa-Black.woff │ │ ├── Bossa-Black.woff2 │ │ ├── Bossa-Bold.woff2 │ │ ├── Bossa-Light.woff │ │ ├── Bossa-Light.woff2 │ │ ├── Bossa-Medium.woff │ │ ├── Bossa-Medium.woff2 │ │ ├── Bossa-Regular.woff │ │ ├── Bossa-Regular.woff2 │ │ ├── UniversalSans-Italic.woff │ │ ├── UniversalSans-Italic.woff2 │ │ ├── UniversalSans-Regular.woff │ │ ├── UniversalSans-Regular.woff2 │ │ └── stylesheet.css │ ├── icon.svg │ ├── flag.svg │ ├── icons │ │ └── connectors │ │ │ ├── frame.svg │ │ │ ├── coinbase.svg │ │ │ ├── metamask.svg │ │ │ └── walletconnect.svg │ └── logo.svg ├── .env.mainnet ├── .env.sepolia ├── postcss.config.js ├── lib │ ├── date.ts │ ├── nation-token.ts │ ├── network-id.test.ts │ ├── network-id.ts │ ├── date.test.ts │ ├── use-handle-error.ts │ ├── use-preferred-network.ts │ ├── approve.ts │ ├── passport-expiration.ts │ ├── passport-expiration-hook.ts │ ├── passport-expiration.test.ts │ ├── sign-agreement.ts │ ├── numbers.test.ts │ ├── use-wagmi.ts │ ├── numbers.ts │ ├── static-call.ts │ ├── connectors.ts │ ├── balancer.ts │ ├── passport-nft.ts │ ├── config.ts │ ├── ve-token.ts │ └── liquidity-rewards.ts ├── cypress.config.ts ├── next-env.d.ts ├── .eslintrc ├── jest.config.js ├── .prettierrc.json ├── components │ ├── EthersInput.tsx │ ├── GradientLink.tsx │ ├── Confetti.tsx │ ├── PassportCheck.tsx │ ├── PassportExpiration.tsx │ ├── PreferredNetworkWrapper.tsx │ ├── Balance.tsx │ ├── ErrorCard.tsx │ ├── HomeCard.tsx │ ├── Head.tsx │ ├── TimeRange.tsx │ ├── MainCard.tsx │ ├── SwitchNetworkBanner.tsx │ ├── ErrorProvider.tsx │ ├── ActionButton.tsx │ ├── ActionNeedsTokenApproval.tsx │ └── Layout.tsx ├── styles │ ├── globals.css │ └── Home.module.css ├── cypress │ └── e2e │ │ ├── lock.cy.ts │ │ ├── app.cy.ts │ │ └── liquidity.cy.ts ├── .gitignore ├── pages │ ├── _document.tsx │ ├── api │ │ └── store-signature.ts │ ├── _app.tsx │ ├── index.tsx │ ├── citizen.tsx │ ├── join.tsx │ └── liquidity.tsx ├── tailwind.config.js ├── README.md ├── package.json ├── abis │ └── ERC20.json └── tsconfig.json └── README.md /contracts/deployments/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nation3/developer-level-4 2 | -------------------------------------------------------------------------------- /ui/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 40..80 3 | round: down 4 | precision: 2 5 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/social.jpg -------------------------------------------------------------------------------- /ui/.env.mainnet: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN=mainnet 2 | INFURA_ID= 3 | ALCHEMY_ID= 4 | ETHERSCAN_ID= 5 | NFTSTORAGE_KEY= -------------------------------------------------------------------------------- /ui/public/passport/art3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/passport/art3.png -------------------------------------------------------------------------------- /ui/public/passport/art4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/passport/art4.png -------------------------------------------------------------------------------- /ui/.env.sepolia: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN=sepolia 2 | INFURA_ID= 3 | ALCHEMY_ID= 4 | ETHERSCAN_ID= 5 | NFTSTORAGE_KEY= 6 | -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff2 -------------------------------------------------------------------------------- /ui/lib/date.ts: -------------------------------------------------------------------------------- 1 | 2 | export const dateToReadable = (date: Date | undefined): string | undefined => { 3 | if (!date) return undefined; 4 | return date.toISOString().substring(0, 10) 5 | } 6 | -------------------------------------------------------------------------------- /ui/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:3000', 6 | supportFile: false 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /ui/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"], 3 | "rules": { 4 | "sort-imports": 0, 5 | "react/no-unescaped-entities": 0, 6 | "jsx-a11y/alt-text": [0], 7 | "react-hooks/exhaustive-deps": "error" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | coverageThreshold: { 6 | global: { 7 | lines: 11.00 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/lib/nation-token.ts: -------------------------------------------------------------------------------- 1 | import { nationToken } from '../lib/config' 2 | import { useBalance } from './use-wagmi' 3 | 4 | export function useNationBalance(address: any) { 5 | return useBalance({ 6 | address: address, 7 | token: nationToken, 8 | watch: true, 9 | enabled: address, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /ui/lib/network-id.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals' 2 | import { networkToId } from './network-id' 3 | 4 | 5 | test("networkToId sepolia", () => { 6 | const actual = networkToId('sepolia') 7 | expect(actual).toBe(11155111) 8 | }) 9 | 10 | test("networkToId mainnet", () => { 11 | const actual = networkToId('mainnet') 12 | expect(actual).toBe(1) 13 | }) 14 | -------------------------------------------------------------------------------- /ui/lib/network-id.ts: -------------------------------------------------------------------------------- 1 | export function networkToId(network: any) { 2 | if (network == undefined) { 3 | return 1 4 | } 5 | switch (network.toLowerCase()) { 6 | case 'mainnet': 7 | return 1 8 | case 'ethereum': 9 | return 1 10 | case 'sepolia': 11 | return 11155111 12 | case 'local': 13 | return 31337 14 | default: 15 | return 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "arrowParens": "always", 8 | "importOrder": [ 9 | "^.*/lib/.*$", 10 | "^.*/components/.*$", 11 | "^.*/pages/(?!.*.css$).*$", 12 | "^[./](?!.*.css$).*$", 13 | "^.*(css)$" 14 | ], 15 | "importOrderSeparation": false 16 | } 17 | -------------------------------------------------------------------------------- /ui/public/passport/icon-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" # See documentation for possible values 7 | directory: "/ui" # Location of package manifests 8 | schedule: 9 | interval: "daily" 10 | open-pull-requests-limit: 1 11 | -------------------------------------------------------------------------------- /ui/components/EthersInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isFixedDecimalsNumber } from '../lib/numbers' 3 | 4 | export default function EthersInput({ onChange, ...props }: any) { 5 | const onInputChange = (e: any) => { 6 | const value = e.target.value 7 | const isValid = isFixedDecimalsNumber(value) 8 | if (isValid) { 9 | onChange(value) 10 | } 11 | } 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /ui/components/GradientLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | export default function GradientLink({ text, href, textSize }: any) { 5 | return ( 6 | 11 | {text} → 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /ui/lib/date.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals' 2 | import { BigNumber } from 'ethers' 3 | import { dateToReadable } from './date' 4 | 5 | test("dateToReadable - February", () => { 6 | const actual = dateToReadable(new Date(2022, 1, 15, 23, 59)) 7 | expect(actual).toBe('2022-02-15') 8 | }) 9 | 10 | test("dateToReadable - December", () => { 11 | const actual = dateToReadable(new Date(2024, 11, 31, 23, 59)) 12 | expect(actual).toBe('2024-12-31') 13 | }) 14 | -------------------------------------------------------------------------------- /ui/lib/use-handle-error.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useErrorContext } from '../components/ErrorProvider' 3 | 4 | // For some contract interactions, a reverted call is not an error 5 | export function useHandleError(object: any, throwOnRevert = true) { 6 | const {addError} = useErrorContext() 7 | useEffect(() => { 8 | if (throwOnRevert && object.error) { 9 | addError([object.error]) 10 | } 11 | }, [object.error, throwOnRevert, addError]) 12 | return object 13 | } 14 | -------------------------------------------------------------------------------- /ui/components/Confetti.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState, useEffect } from 'react' 3 | import Confetti from 'react-confetti' 4 | import { useTimeout } from 'react-use' 5 | 6 | export default function ConfettiComponent() { 7 | const [width, setWidth] = useState(0) 8 | const [height, setHeight] = useState(0) 9 | const [isComplete] = useTimeout(5000) 10 | 11 | useEffect(() => { 12 | setWidth(window.innerWidth) 13 | setHeight(window.innerHeight) 14 | }, []) 15 | 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /ui/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('/fonts/stylesheet.css'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | html, 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 12 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 13 | /* @apply font-body; */ 14 | } 15 | 16 | a { 17 | color: inherit; 18 | text-decoration: none; 19 | } 20 | 21 | * { 22 | box-sizing: border-box; 23 | } 24 | 25 | .dark-card { 26 | @apply bg-slate-800 border border-slate-400; 27 | } -------------------------------------------------------------------------------- /ui/cypress/e2e/lock.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Lock tokens', () => { 2 | 3 | it('lock amount: type more than 18 decimals', () => { 4 | cy.visit('/lock') 5 | 6 | // Expect empty value 7 | cy.get('#lockAmount') 8 | .invoke('val') 9 | .then(val => { 10 | expect(val).to.equal('') 11 | }) 12 | 13 | // Type 19 decimals 14 | cy.get('#lockAmount').type('0.1919191919191919191') 15 | 16 | // Expect 18 decimals 17 | cy.get('#lockAmount') 18 | .invoke('val') 19 | .then(val => { 20 | expect(val).to.equal('0.191919191919191919') 21 | }) 22 | }) 23 | }) 24 | 25 | export {} 26 | -------------------------------------------------------------------------------- /ui/components/PassportCheck.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useHasPassport } from '../lib/passport-nft' 3 | 4 | export default function PassportCheck({ 5 | children, 6 | address, 7 | onPassportChecked, 8 | }: React.PropsWithChildren<{ 9 | address: string 10 | onPassportChecked: (hasPassport: boolean) => void 11 | }>) { 12 | const { hasPassport, isLoading: hasPassportLoading } = useHasPassport(address) 13 | useEffect(() => { 14 | if (!hasPassportLoading) { 15 | onPassportChecked(hasPassport) 16 | } 17 | }, [hasPassport, hasPassportLoading, onPassportChecked]) 18 | 19 | return <>{children} 20 | } 21 | -------------------------------------------------------------------------------- /ui/components/PassportExpiration.tsx: -------------------------------------------------------------------------------- 1 | import { ClockIcon } from '@heroicons/react/24/outline' 2 | import { dateToReadable } from '../lib/date' 3 | 4 | interface Props { 5 | date: Date | undefined 6 | } 7 | 8 | export default function PassportExpiration({ date }: Props) { 9 | return ( 10 |
11 |
12 | 13 |
14 |
Passport expiration date
15 |
16 | {!!date ? (date > new Date() ? dateToReadable(date) : 'Expired') : '-'} 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/components/PreferredNetworkWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import usePreferredNetwork from '../lib/use-preferred-network' 3 | import { useNetwork } from '../lib/use-wagmi' 4 | import SwitchNetworkBanner from './SwitchNetworkBanner' 5 | 6 | export default function PreferredNetworkWrapper({ children }: any) { 7 | const { chain: activeChain } = useNetwork() 8 | const { isPreferredNetwork, preferredNetwork } = usePreferredNetwork() 9 | 10 | return ( 11 | <> 12 | {!isPreferredNetwork && activeChain?.id && ( 13 | 14 | )} 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /ui/components/Balance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NumberType, transformNumber } from '../lib/numbers' 3 | 4 | export default function Balance({ 5 | loading = false, 6 | balance, 7 | prefix = '', 8 | suffix = '', 9 | decimals = 2, 10 | }: any) { 11 | return ( 12 | <> 13 | {loading ? ( 14 | 15 | ) : balance ? ( 16 | `${prefix}${transformNumber( 17 | balance, 18 | NumberType.string, 19 | decimals, 20 | )}${suffix}` 21 | ) : ( 22 | 0 23 | )} 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # yarn 28 | .yarn 29 | 30 | # local env files 31 | .env 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # cypress 41 | cypress/downloads 42 | cypress/screenshots 43 | cypress/videos 44 | -------------------------------------------------------------------------------- /ui/cypress/e2e/app.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Nation3 App UI', () => { 2 | 3 | it('should load the index page', () => { 4 | cy.visit('/') 5 | cy.get('h1').contains('Welcome to Nation3') 6 | }), 7 | 8 | it('should load the /join page', () => { 9 | cy.visit('/join') 10 | cy.get('h2').contains('Become a Nation3 citizen') 11 | }), 12 | 13 | it('should load the /lock page', () => { 14 | cy.visit('/lock') 15 | cy.get('h2').contains('Lock $NATION to get $veNATION') 16 | }), 17 | 18 | it('should load the /liquidity page', () => { 19 | cy.visit('/liquidity') 20 | cy.get('h2').contains('$NATION liquidity rewards') 21 | }) 22 | }) 23 | 24 | export {} 25 | -------------------------------------------------------------------------------- /ui/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/public/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/lib/use-preferred-network.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { networkToId } from './network-id' 3 | import { useNetwork } from './use-wagmi' 4 | 5 | const preferredNetwork = process.env.NEXT_PUBLIC_CHAIN 6 | 7 | export default function usePreferredNetwork() { 8 | const { chain: activeChain } = useNetwork() 9 | 10 | const [isPreferredNetwork, setIsPreferredNetwork] = useState(false) 11 | 12 | useEffect(() => { 13 | if (activeChain?.id && activeChain?.id == networkToId(preferredNetwork)) { 14 | setIsPreferredNetwork(true) 15 | } else { 16 | setIsPreferredNetwork(false) 17 | } 18 | }, [activeChain?.id]) 19 | 20 | return { isPreferredNetwork, preferredNetwork } 21 | } 22 | -------------------------------------------------------------------------------- /ui/public/icons/connectors/frame.svg: -------------------------------------------------------------------------------- 1 | 2 | FrameLogo4 3 | 4 | -------------------------------------------------------------------------------- /ui/lib/approve.ts: -------------------------------------------------------------------------------- 1 | import ERC20 from '../abis/ERC20.json' 2 | import { useContractRead } from './use-wagmi' 3 | import { useContractWrite } from './use-wagmi' 4 | 5 | export function useTokenAllowance({ token, address, spender }: any) { 6 | return useContractRead( 7 | { 8 | address: token, 9 | abi: ERC20.abi, 10 | watch: true, 11 | enabled: Boolean(token && address && spender), 12 | }, 13 | 'allowance', 14 | [address, spender] 15 | ) 16 | } 17 | 18 | export function useTokenApproval({ amountNeeded, token, spender }: any) { 19 | return useContractWrite( 20 | { 21 | address: token, 22 | abi: ERC20.abi, 23 | }, 24 | 'approve', 25 | [spender, amountNeeded] 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /ui/lib/passport-expiration.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | const fourYearsInSeconds = 31556926 * 4 4 | const maxLockPeriod = ethers.BigNumber.from(fourYearsInSeconds); 5 | 6 | // threshold <= (lock amount) * (lock end - expiration) / (max lock period) 7 | // threshold * (max lock period) / (lock amount) <= (lock end - expiration) 8 | // expiration <= (lock end) - threshold * (max lock period) / (lock amount) 9 | export function getPassportExpirationDate(lockAmount: ethers.BigNumber, lockEnd: ethers.BigNumber, threshold: ethers.BigNumber): Date | undefined { 10 | if (lockAmount.isZero()) return undefined; 11 | const expiration = lockEnd.sub(threshold.mul(maxLockPeriod).div(lockAmount)); 12 | return new Date(expiration.mul(1000).toNumber()); 13 | } 14 | -------------------------------------------------------------------------------- /ui/components/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useErrorContext } from './ErrorProvider' 3 | 4 | export default function ErrorCard({ error }: any) { 5 | const { removeError } = useErrorContext() 6 | 7 | return ( 8 |
9 |
10 |

Oopsie

11 |

12 | {error?.message || 13 | error?.reason || 14 | error?.data?.message || 15 | 'Unknown error'} 16 |

17 |
18 |
removeError(error.key)} 21 | > 22 | ✕ 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /contracts/deployments/sepolia.json: -------------------------------------------------------------------------------- 1 | { 2 | "nationToken": "0x23Ca3002706b71a440860E3cf8ff64679A00C9d7", 3 | "veNationToken": "0x8100e77899C24b0F7B516153F84868f850C034BF", 4 | "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", 5 | "balancerPoolId": "0x1fe2f25dff7272e969fcb8b0fb978a9a6a18471b00020000000000000000007a", 6 | "balancerLPToken": "0x1FE2F25Dff7272E969fcb8b0FB978a9A6a18471b", 7 | "lpRewardsContract": "0x18C82f66F8caC20f6b066fd99e67f302AACcbc40", 8 | "nationPassportNFT": "0x11f30642277A70Dab74C6fAF4170a8b340BE2f98", 9 | "nationPassportNFTIssuer": "0xdad32e13E73ce4155a181cA0D350Fee0f2596940", 10 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL", 11 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link" 12 | } 13 | -------------------------------------------------------------------------------- /ui/lib/passport-expiration-hook.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { useMemo } from "react"; 3 | import { getPassportExpirationDate } from "./passport-expiration"; 4 | import { usePassportRevokeUnderBalance } from "./passport-nft"; 5 | import { useAccount } from "./use-wagmi"; 6 | import { useVeNationLock } from "./ve-token"; 7 | export function usePassportExpirationDate(): Date | undefined { 8 | const { address } = useAccount() 9 | const { data: veNationLock } = useVeNationLock(address) 10 | 11 | const { data: threshold } = usePassportRevokeUnderBalance() 12 | 13 | return useMemo(() => { 14 | if (!veNationLock) { 15 | return undefined; 16 | } 17 | 18 | const [lockAmount, lockEnd]: [ethers.BigNumber, ethers.BigNumber] = veNationLock; 19 | return getPassportExpirationDate(lockAmount, lockEnd, threshold); 20 | }, [veNationLock, threshold]); 21 | } 22 | -------------------------------------------------------------------------------- /ui/components/HomeCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import GradientLink from '../components/GradientLink' 3 | 4 | export default function HomeCard({ 5 | href, 6 | icon, 7 | title, 8 | children, 9 | linkText, 10 | }: any) { 11 | return ( 12 | 13 |
14 |
15 | {icon} 16 |

17 | {title} 18 |

19 | 20 | {children} 21 | 22 |
23 | {linkText} → 24 |
25 |
26 |
27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /ui/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import React from 'react' 3 | 4 | export default function WebsiteHead({ title }: any) { 5 | const description = 'Citizen app for the citizens of Nation3' 6 | const image = 'https://app.nation3.org/social.jpg' 7 | 8 | return ( 9 | 10 | Nation3 | {title} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /contracts/deployments/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "nationToken": "0x333A4823466879eeF910A04D473505da62142069", 3 | "veNationToken": "0xF7deF1D2FBDA6B74beE7452fdf7894Da9201065d", 4 | "nationDropContracts": [ 5 | "0xcab2B7614351649870e4DCC3490Ab692bf3beD60", 6 | "0x0B8107EaCeC5e81Fe6E8594dB95E03CF50685f84" 7 | ], 8 | "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", 9 | "balancerPoolId": "0x0bf37157d30dfe6f56757dcadff01aed83b08cd600020000000000000000019a", 10 | "balancerLPToken": "0x0BF37157d30dFe6f56757DCadff01AEd83b08cD6", 11 | "lpRewardsContract": "0xB23F8CC0e77332fE42b426D4AB3c9A893B9798a9", 12 | "nationPassportNFT": "0x3337dac9F251d4E403D6030E18e3cfB6a2cb1333", 13 | "nationPassportNFTIssuer": "0x279c0b6bfCBBA977eaF4ad1B2FFe3C208aa068aC", 14 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL", 15 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link" 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ui_mainnet.yml: -------------------------------------------------------------------------------- 1 | name: /ui (Mainnet) CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | defaults: 15 | run: 16 | working-directory: ui 17 | 18 | strategy: 19 | matrix: 20 | node-version: [18.x, 20.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | cache-dependency-path: ui/yarn.lock 31 | - run: npm install --global yarn 32 | - run: echo $(yarn --version) 33 | - run: cp .env.mainnet .env.local 34 | - run: yarn install 35 | - run: yarn build 36 | - run: yarn lint 37 | - run: yarn e2e:headless 38 | - run: yarn test 39 | - run: yarn test:coverage 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Related GitHub Issue 5 | 6 | 7 | closes #<👉INSERT_ISSUE_NUMBER_HERE👈> 8 | 9 | ## Screenshots (if appropriate): 10 | 11 | 12 | 13 | _Before_ code change: 14 | > ![]() 15 | 16 | _After_ code change: 17 | > ![]() 18 | 19 | ## How Has This Change Been Tested? 20 | 21 | 22 | 23 | 24 | 25 | - [ ] All [status checks](https://github.com/nation3/citizen-app/blob/main/.github/workflows/ui_mainnet.yml#L35) pass (build, lint, e2e, test) 26 | - [ ] Works on Sepolia preview deployment 27 | - [ ] Works on Mainnet preview deployment 28 | 29 | ## Are Any Admin Tasks Required? 30 | 31 | 32 | - [x] No admin tasks 33 | -------------------------------------------------------------------------------- /ui/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | import React from 'react' 3 | 4 | class WebsiteDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 22 | 23 | {/* */} 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | export default WebsiteDocument 38 | -------------------------------------------------------------------------------- /ui/public/icons/connectors/coinbase.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ui/lib/passport-expiration.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { BigNumber } from 'ethers'; 3 | import { getPassportExpirationDate } from './passport-expiration'; 4 | 5 | test("returns undefined if no lock amount", () => { 6 | const date = getPassportExpirationDate(BigNumber.from(0), BigNumber.from(0), BigNumber.from(String(1.5 * 10 ** 18))); 7 | expect(date).toBe(undefined); 8 | }) 9 | 10 | const inFourYears = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 4; 11 | 12 | test("returns past date if below threshold", () => { 13 | const date = getPassportExpirationDate(BigNumber.from(1).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18))); 14 | expect(date).not.toBe(undefined); 15 | expect(date!.getTime() < Date.now()).toBe(true); 16 | }) 17 | 18 | test("returns future date if over threshold", () => { 19 | const date = getPassportExpirationDate(BigNumber.from(2).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18))); 20 | expect(date).not.toBe(undefined); 21 | expect(date!.getTime() > Date.now()).toBe(true); 22 | }) 23 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './pages/**/*.{js,ts,jsx,tsx}', 4 | './components/**/*.{js,ts,jsx,tsx}', 5 | ], 6 | theme: { 7 | fontFamily: { 8 | display: ['Poppins', 'sans-serif'], 9 | body: ['UniversalSans', 'sans-serif'], 10 | }, 11 | fontWeight: { 12 | light: 200, 13 | normal: 300, 14 | medium: 400, 15 | semibold: 500, 16 | bold: 500, 17 | }, 18 | extend: { 19 | colors: { 20 | n3blue: '#69C9FF', 21 | n3green: '#88F1BB', 22 | 'n3blue-100': '#DCFFFF', 23 | 'n3green-100': '#D5FFFF', 24 | n3bg: '#F4FAFF', 25 | n3nav: '#7395B2', 26 | }, 27 | }, 28 | }, 29 | plugins: [require('daisyui')], 30 | daisyui: { 31 | themes: [ 32 | { 33 | mytheme: { 34 | primary: '#69C9FF', 35 | secondary: '#88F1BB', 36 | accent: '#88F1BB', 37 | neutral: '#3d4451', 38 | 'primary-content': '#ffffff', 39 | 'base-100': '#ffffff', 40 | 'base-content': '#224059', 41 | }, 42 | }, 43 | ], 44 | }, 45 | darkMode: 'media', 46 | } 47 | -------------------------------------------------------------------------------- /ui/components/TimeRange.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function TimeRange({ 4 | time, 5 | min, 6 | max, 7 | displaySteps, 8 | onChange, 9 | }: any) { 10 | const step = (min * 100) / max 11 | 12 | return ( 13 | <> 14 | { 20 | onChange(new Date(parseFloat(e.target.value))) 21 | }} 22 | className="range range-secondary mt-4" 23 | step={step || 0} 24 | /> 25 | 26 |
27 | {displaySteps ? ( 28 | <> 29 | - 30 | 31 | 1 y 32 | 33 | 2 y 34 | 35 | 3 y 36 | 37 | ) : ( 38 | <> 39 | {' '} 40 | {new Date(min).toISOString().substring(0, 10)} 41 | 42 | 43 | 44 | 45 | )} 46 | 47 | 4 y 48 |
49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nation3 Citizen App 2 | 3 | [![Vercel](https://vercelbadge.vercel.app/api/nation3/citizen-app)](https://vercel.com/nation3dao/citizen-app/deployments) 4 | 5 | 6 | 7 | --- 8 | 9 | [![](/ui/public/logo.svg)](https://app.nation3.org) 10 | 11 | The Nation3 Citizen App at https://app.nation3.org is where people can connect their Ethereum wallet and interact with the [foundational](https://github.com/nation3/foundations) Nation3 smart contracts. 12 | 13 | > [![app](https://github.com/user-attachments/assets/e1a82323-1750-4993-8b82-d9ee9f529b7c)](https://app.nation3.org) 14 | 15 | 16 | ## File Structure 17 | 18 | The code in this repository is structured into two main parts: 19 | 20 | ``` 21 | . 22 | ├── contracts # The smart contracts 23 | └── ui # The user interface (UI) for interacting with the smart contracts 24 | ``` 25 | 26 | ## Run the UI locally 27 | 28 | See [ui/README.md](ui/README.md) 29 | -------------------------------------------------------------------------------- /ui/components/MainCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function MainCard({ 4 | children, 5 | title, 6 | loading, 7 | gradientBg, 8 | maxWidthClassNames, 9 | }: any) { 10 | return ( 11 | <> 12 | {!loading ? ( 13 |
18 |
23 |
24 |

29 | {title} 30 |

31 | {children} 32 |
33 |
34 |
35 | ) : ( 36 | 37 | )} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/ui_sepolia.yml: -------------------------------------------------------------------------------- 1 | name: /ui (Sepolia) CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | defaults: 15 | run: 16 | working-directory: ui 17 | 18 | strategy: 19 | matrix: 20 | node-version: [18.x, 20.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | cache-dependency-path: ui/yarn.lock 31 | - run: npm install --global yarn 32 | - run: echo $(yarn --version) 33 | - run: cp .env.sepolia .env.local 34 | - run: yarn install 35 | - run: yarn build 36 | - run: yarn lint 37 | - run: yarn e2e:headless 38 | - run: yarn test 39 | - run: yarn test:coverage 40 | - name: Upload coverage reports to Codecov 41 | uses: codecov/codecov-action@v4.0.1 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | slug: nation3/citizen-app 45 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Nation3 Citizen App UI 2 | 3 | https://app.nation3.org 4 | 5 | ## Run the UI locally 6 | 7 | Navigate to the folder of the UI app: 8 | ``` 9 | cd ui/ 10 | ``` 11 | 12 | Install the dependencies: 13 | ``` 14 | yarn install 15 | ``` 16 | 17 | Add variables to your local development environment: 18 | ``` 19 | cp .env.mainnet .env.local 20 | ``` 21 | or 22 | ``` 23 | cp .env.sepolia .env.local 24 | ``` 25 | 26 | 27 | Build: 28 | ``` 29 | yarn build 30 | ``` 31 | 32 | Lint: 33 | ``` 34 | yarn lint 35 | ``` 36 | 37 | Start the development server: 38 | ``` 39 | yarn dev 40 | ``` 41 | 42 | Then open http://localhost:3000 in a browser. 43 | 44 | ## Integration Testing 45 | 46 | Run the integration tests: 47 | ``` 48 | yarn cypress 49 | ``` 50 | 51 | Run the integration tests headlessly: 52 | ``` 53 | yarn cypress:headless 54 | ``` 55 | 56 | ## Unit Testing 57 | 58 | Run unit tests: 59 | ``` 60 | yarn test 61 | ``` 62 | 63 | ## Code Coverage 64 | 65 | [![codecov](https://codecov.io/gh/nation3/citizen-app/branch/main/graph/badge.svg)](https://codecov.io/gh/nation3/citizen-app) 66 | 67 | [![codecov](https://codecov.io/gh/nation3/citizen-app/graphs/icicle.svg)](https://codecov.io/gh/nation3/citizen-app) 68 | 69 | Run code coverage: 70 | ``` 71 | yarn test:coverage 72 | ``` 73 | -------------------------------------------------------------------------------- /ui/lib/sign-agreement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | nationPassportNFTIssuer, 3 | nationPassportAgreementStatement, 4 | nationPassportAgreementURI 5 | } from './config' 6 | import { networkToId } from './network-id' 7 | import { useSignTypedData } from './use-wagmi' 8 | 9 | export const domain = { 10 | name: 'PassportIssuer', 11 | version: '1', 12 | chainId: networkToId(process.env.NEXT_PUBLIC_CHAIN), 13 | verifyingContract: nationPassportNFTIssuer, 14 | } 15 | 16 | export const types = { 17 | Agreement: [ 18 | { name: 'statement', type: 'string' }, 19 | { name: 'termsURI', type: 'string' }, 20 | ], 21 | } 22 | 23 | export const value = { 24 | statement: `${nationPassportAgreementStatement}`, 25 | termsURI: `${nationPassportAgreementURI}`, 26 | } 27 | 28 | export function useSignAgreement({ onSuccess }: { onSuccess: Function }) { 29 | return useSignTypedData({ 30 | domain, 31 | types, 32 | value, 33 | onSuccess, 34 | }) 35 | } 36 | 37 | export async function storeSignature(signature: string, tx: string) { 38 | const response = await fetch('/api/store-signature', { 39 | method: 'POST', 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | }, 43 | body: JSON.stringify({ signature, tx }), 44 | }) 45 | return response.json() 46 | } 47 | -------------------------------------------------------------------------------- /ui/public/passport/ballot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/components/SwitchNetworkBanner.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationCircleIcon } from '@heroicons/react/24/outline' 2 | import React from 'react' 3 | import { useNetwork } from '../lib/use-wagmi' 4 | 5 | const chainIds = { 6 | mainnet: 1, 7 | sepolia: 11155111 8 | } 9 | 10 | type Indexable = { 11 | [key: string]: any 12 | } 13 | 14 | export default function SwitchNetworkBanner({ newNetwork }: any) { 15 | const { switchNetwork } = useNetwork() 16 | 17 | const capitalized = (network: any) => 18 | network.charAt(0).toUpperCase() + network.slice(1) 19 | 20 | return ( 21 |
22 |
23 | 24 | This dapp uses {capitalized(newNetwork)} as its preferred network. 25 |
26 | 27 | {/* There's a small chance the wallet used won't support switchNetwork, in which case the user needs to manually switch */} 28 | {/* Also check if we have specified a chain ID for the network */} 29 | {switchNetwork && (chainIds as Indexable)[newNetwork] && ( 30 |
switchNetwork((chainIds as Indexable)[newNetwork])} 33 | > 34 | Switch to {capitalized(newNetwork)} 35 |
36 | )} 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /ui/lib/numbers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals' 2 | import { BigNumber } from 'ethers' 3 | import { NumberType, transformNumber, isFixedDecimalsNumber } from './numbers' 4 | 5 | describe('transformNumber', () => { 6 | test("transformNumber to Number", () => { 7 | const actual = transformNumber(333, NumberType.number) 8 | expect(actual).toBe(Number(333)) 9 | }) 10 | 11 | test("transformNumber to BigNumber", () => { 12 | const actual = transformNumber(333, NumberType.bignumber) 13 | expect(actual).toBeInstanceOf(BigNumber) 14 | }) 15 | 16 | test("transformNumber to String", () => { 17 | const actual = transformNumber(333, NumberType.string) 18 | expect(actual).toBe("333.000000000000000000") 19 | }) 20 | 21 | test("transformNumber to String (2 decimals)", () => { 22 | const actual = transformNumber(333, NumberType.string, 2) 23 | expect(actual).toBe("333.00") 24 | }) 25 | }) 26 | 27 | describe('isFixedDecimalsNumber', () => { 28 | test('Is working for integers', () => { 29 | const isValid = isFixedDecimalsNumber(12345); 30 | expect(isValid).toBe(true) 31 | }) 32 | 33 | test('Fails for too long decimals', () => { 34 | const isValid = isFixedDecimalsNumber('1.111111111111111111111111111111'); 35 | expect(isValid).toBe(false) 36 | }) 37 | 38 | test('Check "decimals" param is working properly', ()=> { 39 | const isValid = isFixedDecimalsNumber(1.12345, 2); 40 | expect(isValid).toBe(false) 41 | }) 42 | }) -------------------------------------------------------------------------------- /ui/pages/api/store-signature.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | import { NFTStorage, Blob } from 'nft.storage' 4 | import { nationPassportNFTIssuer } from '../../lib/config' 5 | import { provider } from '../../lib/connectors' 6 | import { domain, types, value } from '../../lib/sign-agreement' 7 | 8 | const client = new NFTStorage({ 9 | token: process.env.NFTSTORAGE_KEY || '', 10 | }) 11 | 12 | export default async function storeSignature( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | try { 17 | const prov = provider() 18 | 19 | const sender = ethers.utils.verifyTypedData( 20 | domain, 21 | types, 22 | value, 23 | req.body.signature 24 | ) 25 | const { from, to, confirmations } = await prov.getTransaction(req.body.tx) 26 | // To prevent spam attacks: Check that the sender of the transaction is the same as the uploader, 27 | // the receiver is the passport issuer and the transaction is not very old 28 | if ( 29 | sender === from && 30 | to === nationPassportNFTIssuer && 31 | confirmations < 14 * 60 32 | ) { 33 | const data = new Blob([ 34 | JSON.stringify({ v: 1, sig: req.body.signature, tx: req.body.tx }), 35 | ]) 36 | const cid = await client.storeBlob(data) 37 | res.status(200).json({ cid }) 38 | } 39 | } catch (e) { 40 | console.error(e) 41 | res.status(500).json({ 42 | error: `Storing your signature failed. This is your signature: ${req.body.signature}`, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citizen-app", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "cypress": "cypress open", 10 | "cypress:headless": "cypress run", 11 | "e2e": "start-server-and-test dev http://localhost:3000 cypress", 12 | "e2e:headless": "start-server-and-test dev http://localhost:3000 cypress:headless", 13 | "test": "jest", 14 | "test:coverage": "jest --coverage --collectCoverageFrom=./lib/**" 15 | }, 16 | "dependencies": { 17 | "@heroicons/react": "^2.0.11", 18 | "@metamask/eth-sig-util": "^7.0.1", 19 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 20 | "@types/jest": "^29.5.12", 21 | "@types/node": "^20.12.11", 22 | "@types/react": "18.0.1", 23 | "@types/react-dom": "^18.0.4", 24 | "autoprefixer": "^10.4.0", 25 | "cypress": "^13.13.3", 26 | "daisyui": "^2.13.5", 27 | "eslint": "8.57.0", 28 | "eslint-config-next": "13", 29 | "eslint-config-prettier": "^9.1.0", 30 | "ethers": "^5.7.1", 31 | "jest": "^29.7.0", 32 | "next": "14", 33 | "nft.storage": "^7.1.1", 34 | "postcss": "^8.4.38", 35 | "prettier": "^3.3.3", 36 | "react": "^18.3.1", 37 | "react-animated-3d-card": "^1.0.2", 38 | "react-blockies": "^1.4.1", 39 | "react-confetti": "^6.0.1", 40 | "react-dom": "^18.3.1", 41 | "react-use": "^17.5.0", 42 | "start-server-and-test": "^2.0.5", 43 | "tailwindcss": "^3.4.10", 44 | "ts-jest": "^28.0.8", 45 | "typescript": "^4.7.4", 46 | "use-nft": "^0.12.0", 47 | "wagmi": "0.12.8" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/lib/use-wagmi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useConnect as _useConnect, 3 | useAccount as _useAccount, 4 | useBalance as _useBalance, 5 | useNetwork as _useNetwork, 6 | useContractRead as _useContractRead, 7 | useContractWrite as _useContractWrite, 8 | useSignTypedData as _useSignTypedData, 9 | } from 'wagmi' 10 | import { useStaticCall as _useStaticCall } from './static-call' 11 | import { useHandleError } from './use-handle-error' 12 | 13 | export function useConnect() { 14 | return useHandleError(_useConnect()) 15 | } 16 | 17 | // custom extension of wagmi 18 | export function useStaticCall(params: any) { 19 | return useHandleError(_useStaticCall(params)) 20 | } 21 | 22 | export function useAccount(params?: any) { 23 | return useHandleError(_useAccount(params)) 24 | } 25 | 26 | export function useNetwork() { 27 | return useHandleError(_useNetwork()) 28 | } 29 | 30 | export function useBalance(params: any) { 31 | return useHandleError(_useBalance(params)) 32 | } 33 | 34 | export function useContractRead( 35 | config: any, 36 | functionName: any, 37 | args?: any, 38 | overrides?: any, 39 | throwOnRevert?: any 40 | ) { 41 | return useHandleError( 42 | _useContractRead({ ...config, functionName, args, overrides }), 43 | throwOnRevert 44 | ) 45 | } 46 | 47 | export function useContractWrite( 48 | config: any, 49 | functionName: any, 50 | args: any, 51 | overrides?: any, 52 | throwOnRevert?: any 53 | ) { 54 | return useHandleError( 55 | _useContractWrite({ ...config, functionName, args, overrides }), 56 | throwOnRevert 57 | ) 58 | } 59 | 60 | export function useSignTypedData(params: any) { 61 | return useHandleError(_useSignTypedData(params)) 62 | } 63 | -------------------------------------------------------------------------------- /ui/public/fonts/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Bossa'; 3 | src: url('Bossa-Black.woff2') format('woff2'), 4 | url('Bossa-Black.woff') format('woff'); 5 | font-weight: 900; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: 'Bossa'; 12 | src: url('Bossa-Regular.woff2') format('woff2'), 13 | url('Bossa-Regular.woff') format('woff'); 14 | font-weight: normal; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | @font-face { 20 | font-family: 'Bossa'; 21 | src: url('Bossa-Light.woff2') format('woff2'), 22 | url('Bossa-Light.woff') format('woff'); 23 | font-weight: 300; 24 | font-style: normal; 25 | font-display: swap; 26 | } 27 | 28 | @font-face { 29 | font-family: 'Bossa'; 30 | src: url('Bossa-Medium.woff2') format('woff2'), 31 | url('Bossa-Medium.woff') format('woff'); 32 | font-weight: 500; 33 | font-style: normal; 34 | font-display: swap; 35 | } 36 | 37 | @font-face { 38 | font-family: 'Bossa'; 39 | src: url('Bossa-Bold.woff2') format('woff2'), 40 | url('Bossa-Bold.woff') format('woff'); 41 | font-weight: bold; 42 | font-style: normal; 43 | font-display: swap; 44 | } 45 | 46 | @font-face { 47 | font-family: 'UniversalSans'; 48 | src: url('UniversalSans-Italic.woff2') format('woff2'), 49 | url('UniversalSans-Italic.woff') format('woff'); 50 | font-weight: normal; 51 | font-style: italic; 52 | font-display: swap; 53 | } 54 | 55 | @font-face { 56 | font-family: 'UniversalSans'; 57 | src: url('UniversalSans-Regular.woff2') format('woff2'), 58 | url('UniversalSans-Regular.woff') format('woff'); 59 | font-weight: normal; 60 | font-style: normal; 61 | font-display: swap; 62 | } 63 | -------------------------------------------------------------------------------- /ui/lib/numbers.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from 'ethers' 2 | 3 | function stringToNumber(string: any, decimals: any) { 4 | return Number(string).toFixed(decimals) 5 | } 6 | 7 | export enum NumberType { 8 | number = 'number', 9 | bignumber = 'bignumber', 10 | string = 'string', 11 | } 12 | 13 | export function transformNumber( 14 | num: number | BigNumber | string, 15 | to: NumberType, 16 | decimals = 18 17 | ): BigNumber | string | number { 18 | if (!num) { 19 | return to === NumberType.bignumber ? ethers.BigNumber.from('0') : 0 20 | } 21 | 22 | if (to === NumberType.bignumber) { 23 | if (num instanceof ethers.BigNumber) return num 24 | 25 | return ethers.utils.parseUnits( 26 | typeof num === 'string' ? num : num.toString(), 27 | decimals 28 | ) 29 | } else if (to === NumberType.number) { 30 | if (typeof num === 'number') return num 31 | 32 | if (num instanceof ethers.BigNumber) { 33 | return stringToNumber(ethers.utils.formatUnits(num, 18), decimals) 34 | } else if (typeof num === 'string') { 35 | return parseFloat(num).toFixed(decimals) 36 | } 37 | } else if (to === NumberType.string) { 38 | if (typeof num === 'string') return num 39 | 40 | if (num instanceof ethers.BigNumber) { 41 | return stringToNumber( 42 | ethers.utils.formatUnits(num, 18), 43 | decimals 44 | ).toString() 45 | } else if (typeof num === 'number') { 46 | return num.toFixed(decimals).toString() 47 | } 48 | } 49 | return 0 50 | } 51 | 52 | export function isFixedDecimalsNumber(value: any, decimals = 18) { 53 | const NUMBER_REGEX = RegExp(`^(\\d*\\.{0,1}\\d{0,${decimals}}$)`) 54 | const isValid = value.toString().match(NUMBER_REGEX) 55 | 56 | return Boolean(isValid); 57 | } 58 | -------------------------------------------------------------------------------- /ui/cypress/e2e/liquidity.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Liquidity', () => { 2 | beforeEach(() => { 3 | cy.visit('/liquidity') 4 | 5 | cy.intercept("GET", "https://api.coingecko.com/api/v3/simple/price?ids=nation3,ethereum&vs_currencies=usd", { 6 | body: { 7 | "ethereum": { 8 | "usd": 2672.05 9 | }, 10 | "nation3": { 11 | "usd": 30.75 12 | } 13 | }, 14 | }); 15 | }); 16 | 17 | it('stake: type a deposit amount', () => { 18 | 19 | // Expect default value to be '0' 20 | cy.get('#depositValue') 21 | .invoke('val') 22 | .then(val => { 23 | expect(val).to.equal('0') 24 | }) 25 | 26 | // Clear the default '0' value before typing a number 27 | cy.get('#depositValue').clear() 28 | 29 | // Type a number 30 | cy.get('#depositValue').type('3.3333') 31 | 32 | // Expect the value to no longer be '0' 33 | cy.get('#depositValue') 34 | .invoke('val') 35 | .then(val => { 36 | expect(val).to.equal('3.3333') 37 | }) 38 | }) 39 | 40 | it('unstake: type a withdrawal amount', () => { 41 | 42 | // Click the "Unstake" tab 43 | cy.get("#unstakeTab").click() 44 | 45 | // Expect default value to be '0' 46 | cy.get('#withdrawalValue') 47 | .invoke('val') 48 | .then(val => { 49 | expect(val).to.equal('0') 50 | }) 51 | 52 | // Clear the default '0' value before typing a number 53 | cy.get('#withdrawalValue').clear() 54 | 55 | // Type a number 56 | cy.get('#withdrawalValue').type('0.3333') 57 | 58 | // Expect the value to no longer be '0' 59 | cy.get('#withdrawalValue') 60 | .invoke('val') 61 | .then(val => { 62 | expect(val).to.equal('0.3333') 63 | }) 64 | }) 65 | }) 66 | 67 | export {} 68 | -------------------------------------------------------------------------------- /ui/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { useEffect, useState } from 'react' 3 | import React from 'react' 4 | import { NftProvider } from 'use-nft' 5 | import { WagmiConfig, createClient } from 'wagmi' 6 | import { connectors, provider as externalProvider } from '../lib/connectors' 7 | import { ErrorProvider } from '../components/ErrorProvider' 8 | import Layout from '../components/Layout' 9 | import '../styles/globals.css' 10 | 11 | function App({ Component, pageProps }: any) { 12 | const [client, setClient] = useState() 13 | 14 | useEffect(() => { 15 | let provider = externalProvider 16 | 17 | const userProvider = 18 | window.ethereum || (window as unknown as any).web3?.currentProvider 19 | if (userProvider && process.env.NEXT_PUBLIC_CHAIN !== 'local') { 20 | provider = () => { 21 | console.log( 22 | `Provider: Connected to the user's provider on chain with ID ${parseInt( 23 | userProvider.networkVersion, 24 | )}`, 25 | ) 26 | return new ethers.providers.Web3Provider( 27 | userProvider, 28 | process.env.NEXT_PUBLIC_CHAIN, 29 | ) 30 | } 31 | } 32 | setClient( 33 | createClient({ 34 | autoConnect: true, 35 | connectors, 36 | provider: provider(), 37 | }), 38 | ) 39 | }, []) 40 | 41 | return ( 42 | <> 43 | {client && ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | )} 54 | 55 | ) 56 | } 57 | 58 | export default App 59 | -------------------------------------------------------------------------------- /ui/public/passport/contactless.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/lib/static-call.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | import { useContract, useSigner } from 'wagmi' 3 | 4 | // Hook for calling a contract write method without executing, getting the result without changing state (and thus gas fees) 5 | export function useStaticCall({ 6 | address, 7 | abi, 8 | methodName, 9 | args, 10 | defaultData, 11 | skip = false, 12 | 13 | // set to false if you'd like a reverted txn to be ignored and use the default data 14 | throwOnRevert = true, 15 | 16 | genericErrorMessage = 'Error calling contract', 17 | }: any) { 18 | args = args?.length === 0 ? undefined : args // args should be undefined if there are no arguments, make sure here 19 | 20 | const [loading, setLoading] = useState(true) 21 | const [error, setError] = useState(null) 22 | const [data, setData] = useState(defaultData) 23 | 24 | const { data: signer } = useSigner() 25 | 26 | const contract = useContract({ 27 | address, 28 | abi, 29 | signerOrProvider: signer, 30 | })! 31 | 32 | const stringifiedArgs = useMemo(() => JSON.stringify(args), [args]); 33 | useEffect(() => { 34 | const call = async () => { 35 | if (skip || !signer) { 36 | return 37 | } 38 | try { 39 | const result = args 40 | ? await contract.callStatic[methodName](...args) 41 | : await contract.callStatic[methodName]() 42 | setData(result) 43 | } catch (error) { 44 | if (throwOnRevert) { 45 | console.error(error) 46 | setError({ ...(error as any), message: genericErrorMessage }) 47 | } 48 | } finally { 49 | setLoading(false) 50 | } 51 | } 52 | 53 | call() 54 | // eslint-disable-next-line react-hooks/exhaustive-deps 55 | }, [ 56 | skip, 57 | address, 58 | abi, 59 | methodName, 60 | genericErrorMessage, 61 | signer, 62 | stringifiedArgs, 63 | ]) 64 | 65 | return { 66 | data, 67 | error, 68 | loading, 69 | method: contract.callStatic[methodName], 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ui/lib/connectors.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { configureChains, } from 'wagmi' 3 | import { mainnet, sepolia } from 'wagmi/chains' 4 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet' 5 | import { InjectedConnector } from 'wagmi/connectors/injected' 6 | import { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect' 7 | import CoinbaseWalletIcon from '../public/icons/connectors/coinbase.svg' 8 | import FrameIcon from '../public/icons/connectors/frame.svg' 9 | import MetaMaskIcon from '../public/icons/connectors/metamask.svg' 10 | import WalletConnectIcon from '../public/icons/connectors/walletconnect.svg' 11 | import { networkToId } from './network-id' 12 | 13 | const chains = [mainnet, sepolia] 14 | 15 | export function provider() { 16 | if (process.env.NEXT_PUBLIC_CHAIN === 'local') { 17 | console.log('Provider: Connected to localhost provider') 18 | return new ethers.providers.JsonRpcProvider( 19 | 'http://127.0.0.1:7545', 20 | networkToId(process.env.NEXT_PUBLIC_CHAIN) 21 | ) 22 | } else { 23 | console.log( 24 | `Provider: Connected to the external provider on chain ${process.env.NEXT_PUBLIC_CHAIN}` 25 | ) 26 | return ethers.getDefaultProvider(process.env.NEXT_PUBLIC_CHAIN, { 27 | infura: process.env.NEXT_PUBLIC_INFURA_ID, 28 | alchemy: process.env.NEXT_PUBLIC_ALCHEMY_ID, 29 | quorum: 1, 30 | }); 31 | } 32 | } 33 | 34 | export const connectors = [ 35 | new InjectedConnector({ 36 | chains, 37 | options: { shimDisconnect: true }, 38 | }), 39 | new WalletConnectConnector({ 40 | chains, 41 | options: { 42 | showQrModal: true, 43 | projectId: 'de21254f0716238419606243642a9266', 44 | }, 45 | }), 46 | new CoinbaseWalletConnector({ 47 | chains, 48 | options: { 49 | appName: 'Nation3 app', 50 | }, 51 | }), 52 | ] 53 | 54 | export const connectorIcons = { 55 | Frame: FrameIcon, 56 | MetaMask: MetaMaskIcon, 57 | WalletConnect: WalletConnectIcon, 58 | 'Coinbase Wallet': CoinbaseWalletIcon, 59 | } -------------------------------------------------------------------------------- /ui/lib/balancer.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import BalancerVault from '../abis/BalancerVault.json' 3 | import { balancerVault } from './config' 4 | import { NumberType, transformNumber } from './numbers' 5 | import { useContractRead } from './use-wagmi' 6 | 7 | export function useBalancerPool(id: any) { 8 | const { data: poolData, isLoading } = useContractRead( 9 | { 10 | address: balancerVault, 11 | abi: BalancerVault.abi, 12 | }, 13 | 'getPoolTokens', 14 | [id], 15 | undefined, 16 | process.env.NEXT_PUBLIC_CHAIN === 'mainnet' 17 | ) 18 | 19 | const [poolValue, setPoolValue] = useState(0) 20 | const [nationPrice, setNationPrice] = useState(0) 21 | const [ethPrice, setEthPrice] = useState(0) 22 | 23 | useEffect(() => { 24 | async function fetchData() { 25 | const priceRes = await fetch( 26 | 'https://api.coingecko.com/api/v3/simple/price?ids=nation3,ethereum&vs_currencies=usd' 27 | ) 28 | const { nation3, ethereum } = await priceRes.json() 29 | setNationPrice(nation3.usd) 30 | setEthPrice(ethereum.usd) 31 | } 32 | fetchData() 33 | }, []) 34 | 35 | useEffect(() => { 36 | if (!isLoading && poolData && nationPrice && ethPrice) { 37 | let nationBalance 38 | let wethBalance 39 | if (process.env.NEXT_PUBLIC_CHAIN === 'mainnet') { 40 | const balances = poolData[1] 41 | nationBalance = balances[0] 42 | wethBalance = balances[1] 43 | } else { 44 | nationBalance = transformNumber(333, NumberType.bignumber) 45 | wethBalance = transformNumber(333, NumberType.bignumber) 46 | } 47 | 48 | if (nationBalance && wethBalance) { 49 | const nationValue = nationBalance.mul(Math.round(nationPrice)) 50 | const ethValue = wethBalance.mul(Math.round(ethPrice)) 51 | const totalValue = nationValue.add(ethValue) 52 | setPoolValue(totalValue) 53 | } 54 | } 55 | }, [isLoading, poolData, ethPrice, nationPrice]) 56 | return { poolValue, nationPrice, isLoading } 57 | } 58 | -------------------------------------------------------------------------------- /ui/public/passport/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/components/ErrorProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useState } from 'react' 2 | import { useNetwork } from 'wagmi' 3 | import { networkToId } from '../lib/network-id' 4 | 5 | const ErrorContext = createContext({} as any) 6 | 7 | function ErrorProvider({ children }: any) { 8 | const [errors, setErrors] = useState([] as any[]) 9 | let [count, setCount] = useState(0) 10 | const { chain: activeChain } = useNetwork() 11 | 12 | const addError = useCallback( 13 | (newErrors: any) => { 14 | if ( 15 | newErrors && 16 | newErrors[0] && 17 | activeChain?.id && 18 | activeChain.id == networkToId(process.env.NEXT_PUBLIC_CHAIN) 19 | ) { 20 | for (let error of newErrors) { 21 | console.error(error) 22 | if (error instanceof Error) { 23 | error = JSON.parse( 24 | JSON.stringify(error, Object.getOwnPropertyNames(error)), 25 | ) 26 | // Don't add the error if it's "invalid address or ENS name", 27 | // no idea why those errors appear in the first place. 28 | if ( 29 | error.code && 30 | (error.code === 'INVALID_ARGUMENT' || 31 | error.code === 'MISSING_ARGUMENT') 32 | ) { 33 | return 34 | } 35 | } 36 | setErrors((prev) => [...prev, { key: prev.length, ...error }]) 37 | setCount((prev) => prev + 1) 38 | } 39 | } 40 | }, 41 | [activeChain?.id], 42 | ) 43 | 44 | const removeError = (key: any) => { 45 | if (key > -1) { 46 | setErrors(errors.filter((error: any) => error.key !== key)) 47 | setCount(--count) 48 | } 49 | } 50 | 51 | const context = { errors, addError, removeError } 52 | return ( 53 | {children} 54 | ) 55 | } 56 | 57 | function useErrorContext() { 58 | const errors = useContext(ErrorContext) 59 | if (errors === undefined) { 60 | throw new Error('useErrorContext must be used within a ErrorProvider') 61 | } 62 | return errors 63 | } 64 | 65 | export { ErrorProvider, useErrorContext } 66 | -------------------------------------------------------------------------------- /ui/lib/passport-nft.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useNft } from 'use-nft' 3 | import PassportIssuer from '../abis/PassportIssuer.json' 4 | import { nationPassportNFT, nationPassportNFTIssuer } from './config' 5 | import { useContractRead, useContractWrite } from './use-wagmi' 6 | 7 | const nftIssuerContractParams = { 8 | address: nationPassportNFTIssuer, 9 | abi: PassportIssuer.abi, 10 | } 11 | 12 | export function useHasPassport(address: any) { 13 | const [hasPassport, setHasPassport] = useState(false) 14 | const { data: passportStatus, isLoading } = usePassportStatus(address) 15 | 16 | useEffect(() => { 17 | if (passportStatus == 1) { 18 | setHasPassport(true) 19 | } 20 | }, [passportStatus, isLoading]) 21 | 22 | return { hasPassport, isLoading } 23 | } 24 | 25 | export function usePassportStatus(address: any) { 26 | return useContractRead( 27 | { 28 | ...nftIssuerContractParams, 29 | watch: true, 30 | enabled: Boolean(address), 31 | }, 32 | 'passportStatus', 33 | [address], 34 | undefined, 35 | false 36 | ) 37 | } 38 | 39 | export function useClaimRequiredBalance() { 40 | return useContractRead( 41 | { 42 | ...nftIssuerContractParams, 43 | watch: true, 44 | }, 45 | 'claimRequiredBalance', 46 | undefined, 47 | ) 48 | } 49 | 50 | export function useClaimPassport() { 51 | return useContractWrite( 52 | { 53 | address: nationPassportNFTIssuer, 54 | abi: PassportIssuer.abi, 55 | }, 56 | 'claim', 57 | undefined 58 | ) 59 | } 60 | 61 | export function usePassport(address: any) { 62 | const { data: id, isLoading: loadingID } = useContractRead( 63 | { 64 | ...nftIssuerContractParams, 65 | enable: Boolean(address) 66 | }, 67 | 'passportId', 68 | [address], 69 | undefined, 70 | false 71 | ) 72 | console.log(`Passport ID ${id}`) 73 | const { loading, nft } = useNft(nationPassportNFT || '', id) 74 | return { data: { id, nft }, isLoading: loadingID || loading } 75 | } 76 | 77 | export function usePassportRevokeUnderBalance() { 78 | return useContractRead( 79 | { 80 | ...nftIssuerContractParams, 81 | }, 82 | 'revokeUnderBalance', 83 | undefined 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /ui/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ui/components/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { ReactNode } from 'react' 3 | import { useWaitForTransaction } from 'wagmi' 4 | import usePreferredNetwork from '../lib/use-preferred-network' 5 | import { useAccount, useNetwork } from '../lib/use-wagmi' 6 | import ActionNeedsTokenApproval from './ActionNeedsTokenApproval' 7 | 8 | export interface ActionButtonProps { 9 | className?: string 10 | children: ReactNode 11 | action: any 12 | preAction?: Function 13 | postAction?: Function 14 | approval?: { 15 | token: string 16 | spender: string 17 | amountNeeded: BigNumber | number | string 18 | approveText: string 19 | allowUnlimited?: boolean 20 | } 21 | } 22 | 23 | export default function ActionButton({ 24 | className, 25 | children, 26 | action, 27 | preAction, 28 | postAction, 29 | approval, 30 | }: ActionButtonProps) { 31 | const { address } = useAccount() 32 | const { writeAsync, data, isLoadingOverride } = action 33 | const { isLoading } = useWaitForTransaction({ 34 | hash: data?.hash, 35 | }) 36 | const onClick = async () => { 37 | preAction && preAction() 38 | const tx = await writeAsync() 39 | tx?.wait && (await tx.wait()) 40 | postAction && postAction() 41 | } 42 | 43 | const { chain: activeChain } = useNetwork() 44 | const { isPreferredNetwork } = usePreferredNetwork() 45 | 46 | return ( 47 | <> 48 | {!isPreferredNetwork ? ( 49 | 52 | ) : !address ? ( 53 | 56 | ) : isLoading || isLoadingOverride ? ( 57 |
58 | 59 |
60 | ) : approval ? ( 61 | 67 | {children} 68 | 69 | ) : ( 70 |
71 | {children} 72 |
73 | )} 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /ui/lib/config.ts: -------------------------------------------------------------------------------- 1 | const zeroAddress = '0x0000000000000000000000000000000000000000' 2 | 3 | interface DeploymentConfig { 4 | nationToken: string, 5 | veNationToken: string, 6 | balancerVault: string, 7 | balancerPoolId: string, 8 | balancerLPToken: string, 9 | lpRewardsContract: string, 10 | nationPassportNFT: string, 11 | nationPassportNFTIssuer: string, 12 | nationPassportAgreementStatement: string, 13 | nationPassportAgreementURI: string, 14 | } 15 | 16 | interface Config { 17 | nationToken: string, 18 | veNationToken: string, 19 | balancerDomain: string, 20 | balancerVault: string, 21 | balancerPoolId: string, 22 | balancerLPToken: string, 23 | etherscanDomain: string, 24 | lpRewardsContract: string, 25 | mobilePassportDomain: string, 26 | nationPassportNFT: string, 27 | nationPassportNFTIssuer: string, 28 | nationPassportAgreementStatement: string, 29 | nationPassportAgreementURI: string, 30 | } 31 | 32 | 33 | const chain = process.env.NEXT_PUBLIC_CHAIN || 'sepolia'; 34 | const defaultConfig = require(`../../contracts/deployments/${chain}.json`) as DeploymentConfig 35 | const config: Config = { 36 | nationToken: defaultConfig.nationToken || zeroAddress, 37 | veNationToken: defaultConfig.veNationToken || zeroAddress, 38 | balancerDomain: chain === 'mainnet' ? 'https://app.balancer.fi/#/ethereum' : `https://app.balancer.fi/#/${chain}`, 39 | balancerVault: defaultConfig.balancerVault || zeroAddress, 40 | balancerPoolId: defaultConfig.balancerPoolId || zeroAddress, 41 | balancerLPToken: defaultConfig.balancerLPToken || zeroAddress, 42 | etherscanDomain: chain === 'mainnet' ? 'https://etherscan.io' : `https://${chain}.etherscan.io`, 43 | lpRewardsContract: defaultConfig.lpRewardsContract || zeroAddress, 44 | mobilePassportDomain: chain === 'mainnet' ? 'https://passports.nation3.org' : `https://mobile-passport-${chain}.vercel.app`, 45 | nationPassportNFT: defaultConfig.nationPassportNFT || zeroAddress, 46 | nationPassportNFTIssuer: defaultConfig.nationPassportNFTIssuer || zeroAddress, 47 | nationPassportAgreementStatement: defaultConfig.nationPassportAgreementStatement || "", 48 | nationPassportAgreementURI: defaultConfig.nationPassportAgreementURI || "", 49 | } 50 | 51 | console.log(config) 52 | 53 | export const { 54 | nationToken, 55 | veNationToken, 56 | balancerDomain, 57 | balancerVault, 58 | balancerPoolId, 59 | balancerLPToken, 60 | etherscanDomain, 61 | lpRewardsContract, 62 | mobilePassportDomain, 63 | nationPassportNFT, 64 | nationPassportNFTIssuer, 65 | nationPassportAgreementStatement, 66 | nationPassportAgreementURI, 67 | } = config 68 | -------------------------------------------------------------------------------- /ui/lib/ve-token.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import VotingEscrow from '../abis/VotingEscrow.json' 3 | import { veNationToken } from '../lib/config' 4 | import { useBalance, useContractRead, useContractWrite } from './use-wagmi' 5 | 6 | const contractParams = { 7 | address: veNationToken, 8 | abi: VotingEscrow.abi, 9 | } 10 | 11 | export function useVeNationBalance(address: any) { 12 | return useBalance({ 13 | address, 14 | token: veNationToken, 15 | watch: true, 16 | enabled: !!address, 17 | }) 18 | } 19 | 20 | let gasLimits = { 21 | locked: 330000, 22 | create_lock: 600000, 23 | increase_amount: 600000, 24 | increase_unlock_time: 600000, 25 | withdraw: 480000, 26 | } 27 | 28 | export function useVeNationLock(address: any) { 29 | return useContractRead( 30 | { 31 | ...contractParams, 32 | watch: true, 33 | enabled: !!address, 34 | }, 35 | 'locked', 36 | [address] 37 | ) 38 | } 39 | 40 | export function useVeNationCreateLock(amount: any, time: any) { 41 | return useContractWrite( 42 | contractParams, 43 | 'create_lock', 44 | [amount, time], 45 | { gasLimit: gasLimits.create_lock }, 46 | ) 47 | } 48 | 49 | export function useVeNationIncreaseLock({ 50 | newAmount, 51 | currentTime, 52 | newTime, 53 | }: any) { 54 | const { writeAsync: increaseLockAmount, data: lockAmountData } = 55 | useVeNationIncreaseLockAmount(newAmount) 56 | const { writeAsync: increaseLockTime, data: lockTimeData } = 57 | useVeNationIncreaseLockTime(newTime) 58 | return useMemo(() => { 59 | if (newAmount && newAmount.gt(0)) { 60 | return { writeAsync: increaseLockAmount, data: lockAmountData } 61 | } 62 | if (newTime && currentTime && newTime.gt(currentTime)) { 63 | return { writeAsync: increaseLockTime, data: lockTimeData } 64 | } 65 | return {} 66 | }, [newAmount, currentTime, newTime, increaseLockAmount, increaseLockTime, lockAmountData, lockTimeData]) 67 | } 68 | 69 | export function useVeNationIncreaseLockAmount(amount: any) { 70 | return useContractWrite( 71 | contractParams, 72 | 'increase_amount', 73 | [amount], 74 | { gasLimit: gasLimits.increase_amount } 75 | ) 76 | } 77 | 78 | export function useVeNationIncreaseLockTime(time: any) { 79 | return useContractWrite( 80 | contractParams, 81 | 'increase_unlock_time', 82 | [time], 83 | { gasLimit: gasLimits.increase_unlock_time } 84 | ) 85 | } 86 | 87 | export function useVeNationWithdrawLock() { 88 | return useContractWrite( 89 | contractParams, 90 | 'withdraw', 91 | [], 92 | { gasLimit: gasLimits.withdraw } 93 | ) 94 | } 95 | 96 | export function useVeNationSupply() { 97 | return useContractRead(contractParams, 'totalSupply') 98 | } 99 | -------------------------------------------------------------------------------- /ui/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | UserPlusIcon, 3 | LockClosedIcon, 4 | PlusIcon, 5 | } from '@heroicons/react/24/outline' 6 | import Image from 'next/image' 7 | import Link from 'next/link' 8 | import React from 'react' 9 | import { balancerDomain, nationToken } from '../lib/config' 10 | import GradientLink from '../components/GradientLink' 11 | import Head from '../components/Head' 12 | import HomeCard from '../components/HomeCard' 13 | import flag from '../public/flag.svg' 14 | 15 | export default function Index() { 16 | return ( 17 | <> 18 | 19 |
20 |

21 | Welcome to Nation3 22 | Flag 23 |

24 | 25 |

26 | Nation3 is a sovereign cloud nation. We are building a community of 27 | like-minded people creating a nation on the cloud.{' '} 28 | 29 |
30 |
31 | Here you can perform on-chain operations related to the Nation3 32 | community, such as... 33 |

34 | 35 |
36 | 40 | } 41 | title="Get $veNATION" 42 | linkText="Get $veNATION" 43 | > 44 |

45 | Lock your $NATION to obtain $veNATION and help govern the Nation3 46 | DAO. $veNATION will be required to mint the upcoming passport NFTs 47 | to become a citizen. 48 |

49 |
50 | 51 | 55 | } 56 | title="Become a citizen" 57 | linkText="Claim a passport" 58 | > 59 |

60 | Once you have $veNATION, you can claim a passport. Only 420 61 | Genesis passports will be launched in the beginning. 62 |

63 |
64 | 65 | } 68 | title="Buy more $NATION" 69 | linkText="Buy $NATION" 70 | > 71 |

72 | You can buy more $NATION and participate in liquidity rewards. You 73 | can also lock your $NATION to get $veNATION to boost your rewards, 74 | get a passport NFT, and govern the DAO. 75 |

76 |
77 |
78 |
79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /ui/components/ActionNeedsTokenApproval.tsx: -------------------------------------------------------------------------------- 1 | import { InformationCircleIcon } from '@heroicons/react/24/outline' 2 | import { BigNumber } from 'ethers' 3 | import { useEffect, useState } from 'react' 4 | import { useTokenAllowance, useTokenApproval } from '../lib/approve' 5 | import { NumberType, transformNumber } from '../lib/numbers' 6 | import { useAccount } from '../lib/use-wagmi' 7 | import ActionButton from './ActionButton' 8 | 9 | export default function ActionNeedsTokenApproval({ 10 | className, 11 | children, 12 | amountNeeded, 13 | token, 14 | spender, 15 | approveText, 16 | action, 17 | preAction, 18 | allowUnlimited = true, 19 | }: any) { 20 | const { address } = useAccount() 21 | const { data: tokenAllowance, isLoading: tokenAllowanceLoading } = 22 | useTokenAllowance({ token, address, spender }) 23 | const [approveUnlimited, setApproveUnlimited] = useState(allowUnlimited) 24 | const [weiAmountNeeded, setWeiAmountNeeded] = useState( 25 | BigNumber.from(0), 26 | ) 27 | 28 | useEffect(() => { 29 | setWeiAmountNeeded( 30 | transformNumber( 31 | amountNeeded?.formatted || amountNeeded, 32 | NumberType.bignumber, 33 | ) as BigNumber, 34 | ) 35 | }, [amountNeeded]) 36 | 37 | const approve = useTokenApproval({ 38 | amountNeeded: approveUnlimited 39 | ? BigNumber.from('1000000000000000000000000000') 40 | : weiAmountNeeded, 41 | token, 42 | spender, 43 | }) 44 | 45 | return ( 46 | <> 47 | {!tokenAllowanceLoading && !approve?.isLoading ? ( 48 | tokenAllowance?.gte(weiAmountNeeded) ? ( 49 | 54 | {children} 55 | 56 | ) : ( 57 |
58 | {allowUnlimited && ( 59 | 76 | )} 77 | 78 | {approveText} 79 | 80 |
81 | ) 82 | ) : ( 83 |
84 | 85 |
86 | )} 87 | 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /ui/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/public/passport/logo-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/pages/citizen.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { useState } from 'react' 3 | // @ts-ignore 4 | import Card from 'react-animated-3d-card' 5 | import { useSignMessage } from 'wagmi' 6 | import { mobilePassportDomain } from '../lib/config' 7 | import { usePassportExpirationDate } from '../lib/passport-expiration-hook' 8 | import { usePassport } from '../lib/passport-nft' 9 | import { useAccount } from '../lib/use-wagmi' 10 | import Confetti from '../components/Confetti' 11 | import Head from '../components/Head' 12 | import PassportExpiration from '../components/PassportExpiration' 13 | import BallotIcon from '../public/passport/ballot.svg' 14 | import DiscordIcon from '../public/passport/discord.svg' 15 | import AddToWallet from '../public/passport/wallet.svg' 16 | 17 | export default function Citizen() { 18 | const { address } = useAccount() 19 | const { data: passportData } = usePassport(address) 20 | const [confettiNumber, setConfettiNumber] = useState>([]) 21 | 22 | const addConfetti = () => { 23 | setConfettiNumber([...confettiNumber, confettiNumber.length]) 24 | } 25 | 26 | const { signMessage: signMessageAndDownloadPass } = useSignMessage({ 27 | message: 'I am the holder of this Nation3 passport', 28 | onSuccess(data) { 29 | console.log('signMessageAndDownloadPass data:', data) 30 | const downloadPassURI: string = `${mobilePassportDomain}/api/downloadPass?address=${address}&signature=${data}&platform=Apple` 31 | console.log('downloadPassURI:', downloadPassURI) 32 | window.location.href = downloadPassURI 33 | }, 34 | onError(error) { 35 | console.error('signMessageAndDownloadPass error:', error) 36 | }, 37 | }) 38 | 39 | const passportExpirationDate = usePassportExpirationDate() 40 | 41 | return ( 42 | <> 43 | {confettiNumber.map((number: Number) => ( 44 | 45 | ))} 46 | 47 | {passportData?.nft ? ( 48 |
49 |
50 |

51 | Welcome, citizen 52 |

53 |
54 |
55 | 420 ? '390px' : '340px', 58 | height: window.innerWidth > 420 ? '450px' : '400px', 59 | cursor: 'pointer', 60 | position: 'relative', 61 | }} 62 | onClick={() => addConfetti()} 63 | > 64 | NFT image 65 | 66 |
67 | 68 |
69 |
70 | 76 | 82 | Discord icon 83 | 84 | Access gated channels 85 | 86 | 87 | 93 | Ballot icon 94 | Vote on proposals 95 | 96 |
97 | 98 |
99 |
100 |
101 |
102 | ) : ( 103 |
104 | 105 |
106 | )} 107 | 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /ui/public/icons/connectors/metamask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/public/passport/art.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /ui/lib/liquidity-rewards.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { useState, useEffect } from 'react' 3 | import { lpRewardsContract, balancerLPToken } from '../lib/config' 4 | import LiquidityRewardsDistributor from '../abis/BoostedLiquidityDistributor.json' 5 | import ERC20 from '../abis/ERC20.json' 6 | import { NumberType, transformNumber } from './numbers' 7 | import { 8 | useBalance, 9 | useStaticCall, 10 | useContractRead, 11 | useContractWrite, 12 | } from './use-wagmi' 13 | 14 | const contractParams = { 15 | address: lpRewardsContract, 16 | abi: LiquidityRewardsDistributor.abi, 17 | } 18 | 19 | export function useLiquidityRewards({ nationPrice, poolValue, address }: any) { 20 | const { data: totalRewards, isLoading: totalRewardsLoading } = 21 | useContractRead(contractParams, 'totalRewards', undefined, undefined, false) 22 | const months = 6 23 | 24 | const { data: unclaimedRewards, loading: unclaimedRewardsLoading } = 25 | useStaticCall({ 26 | ...contractParams, 27 | methodName: 'claimRewards', 28 | defaultData: transformNumber(0, NumberType.bignumber), 29 | throwOnRevert: false, // assumes a reverted transaction means no claimable rewards 30 | skip: !address, 31 | }) 32 | 33 | const { data: userDeposit, isLoading: userDepositLoading } = useContractRead( 34 | { 35 | ...contractParams, 36 | watch: true, 37 | enabled: Boolean(address), 38 | }, 39 | 'userDeposit', 40 | [address] 41 | ) 42 | 43 | const { data: totalDeposit, isLoading: totalDepositLoading } = 44 | useContractRead(contractParams, 'totalDeposit', undefined, undefined, false) 45 | 46 | const { data: lpTokensSupply, isLoading: lpTokensSupplyLoading } = 47 | useContractRead( 48 | { address: balancerLPToken, abi: ERC20.abi }, 49 | 'totalSupply', 50 | undefined, 51 | undefined, 52 | false 53 | ) 54 | 55 | const { data: userBalance, isLoading: userBalanceLoading } = useContractRead( 56 | { 57 | ...contractParams, 58 | watch: true, 59 | enabled: Boolean(address), 60 | }, 61 | 'userBalance', 62 | [address] 63 | ) 64 | 65 | const [liquidityRewardsAPY, setLiquidityRewardsAPY] = useState( 66 | transformNumber(0, NumberType.bignumber) as BigNumber 67 | ) 68 | 69 | useEffect(() => { 70 | if (totalRewards && poolValue && totalDeposit && lpTokensSupply) { 71 | if (totalDeposit.gt(transformNumber(0, NumberType.bignumber))){ 72 | setLiquidityRewardsAPY( 73 | totalRewards 74 | .mul(transformNumber(12 / months, NumberType.bignumber)) 75 | .mul(transformNumber(nationPrice, NumberType.bignumber, 2)) 76 | .div(poolValue.mul(totalDeposit).div(lpTokensSupply)) 77 | ) 78 | } 79 | } 80 | }, [ 81 | poolValue, 82 | totalDeposit, 83 | lpTokensSupply, 84 | nationPrice, 85 | totalRewards, 86 | totalRewardsLoading, 87 | totalDepositLoading, 88 | lpTokensSupplyLoading, 89 | ]) 90 | 91 | return { 92 | liquidityRewardsAPY, 93 | unclaimedRewards, 94 | userDeposit, 95 | totalDeposit, 96 | userBalance, 97 | loading: 98 | totalRewardsLoading || 99 | unclaimedRewardsLoading || 100 | userDepositLoading || 101 | totalDepositLoading || 102 | userBalanceLoading, 103 | } 104 | } 105 | 106 | export function usePoolTokenBalance(address: any) { 107 | return useBalance({ 108 | address: address, 109 | token: balancerLPToken, 110 | watch: true, 111 | enabled: address, 112 | }) 113 | } 114 | 115 | // userDeposit = amount of LP tokens staked by user 116 | // totalDeposit = amount of LP tokens in rewards contract 117 | // userVotingPower = veNationBalance 118 | // totalVotingPower = veNATION supply 119 | export function useVeNationBoost({ 120 | userDeposit, 121 | totalDeposit, 122 | userVeNation, 123 | totalVeNation, 124 | userBalance, 125 | }: any) { 126 | const [boost, setBoost] = useState({ 127 | canBoost: false, 128 | }) 129 | useEffect(() => { 130 | if ( 131 | userDeposit && 132 | totalDeposit && 133 | userVeNation && 134 | totalVeNation && 135 | userBalance 136 | ) { 137 | const n = { 138 | userDeposit: parseFloat( 139 | transformNumber(userDeposit, NumberType.string) as string 140 | ), 141 | totalDeposit: parseFloat( 142 | transformNumber(totalDeposit, NumberType.string) as string 143 | ), 144 | userVeNation: parseFloat( 145 | transformNumber(userVeNation, NumberType.string) as string 146 | ), 147 | totalVeNation: parseFloat( 148 | transformNumber(totalVeNation, NumberType.string) as string 149 | ), 150 | userBalance: parseFloat( 151 | transformNumber(userBalance, NumberType.string) as string 152 | ), 153 | } 154 | 155 | const baseBalance = n.userDeposit * 0.4 156 | 157 | let boostedBalance = 158 | baseBalance + 159 | ((n.totalDeposit * n.userVeNation) / n.totalVeNation) * (60 / 100) 160 | 161 | boostedBalance = Math.min(boostedBalance, n.userDeposit) 162 | 163 | const potentialBoost = boostedBalance / baseBalance 164 | 165 | boostedBalance = Math.min(boostedBalance, n.userDeposit) 166 | 167 | const currentBoost = n.userBalance / baseBalance 168 | 169 | setBoost({ 170 | currentBoost: transformNumber( 171 | Math.max(currentBoost, 1), 172 | NumberType.bignumber 173 | ), 174 | potentialBoost: transformNumber(potentialBoost, NumberType.bignumber), 175 | canBoost: 176 | Math.trunc(potentialBoost * 10) > Math.trunc(currentBoost * 10), 177 | }) 178 | } 179 | }, [userDeposit, totalDeposit, userVeNation, totalVeNation, userBalance]) 180 | 181 | return boost 182 | } 183 | 184 | export function useBoostedAPY({ defaultAPY, boostMultiplier }: any) { 185 | const [apy, setAPY] = useState( 186 | parseFloat(transformNumber(defaultAPY, NumberType.string) as string) 187 | ) 188 | useEffect(() => { 189 | let defaultAPYasNumber = transformNumber( 190 | defaultAPY, 191 | NumberType.number 192 | ) as number 193 | let boostMultiplierAsNumber = transformNumber( 194 | boostMultiplier, 195 | NumberType.number 196 | ) as number 197 | 198 | if (defaultAPYasNumber != 0 && boostMultiplierAsNumber != 0) { 199 | setAPY(defaultAPYasNumber * boostMultiplierAsNumber) 200 | } 201 | }, [defaultAPY, boostMultiplier]) 202 | return apy 203 | } 204 | 205 | // Using Wagmi's contractWrite directly, getting a "no signer connected" error otherwise 206 | export function useClaimRewards() { 207 | return useContractWrite(contractParams, 'claimRewards', undefined, { gasLimit: 300000 }) 208 | } 209 | 210 | export function useDeposit(amount: any) { 211 | return useContractWrite(contractParams, 'deposit', [amount], { gasLimit: 300000 }) 212 | } 213 | 214 | export function useWithdraw(amount: any) { 215 | return useContractWrite(contractParams, 'withdraw', [amount], { gasLimit: 300000 }) 216 | } 217 | 218 | export function useWithdrawAndClaim() { 219 | return useContractWrite(contractParams, 'withdrawAndClaim', undefined, { gasLimit: 300000 }) 220 | } 221 | -------------------------------------------------------------------------------- /ui/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, 4 | { "inputs": [], "name": "CallerIsNotAuthorized", "type": "error" }, 5 | { "inputs": [], "name": "TargetIsZeroAddress", "type": "error" }, 6 | { 7 | "anonymous": false, 8 | "inputs": [ 9 | { 10 | "indexed": true, 11 | "internalType": "address", 12 | "name": "owner", 13 | "type": "address" 14 | }, 15 | { 16 | "indexed": true, 17 | "internalType": "address", 18 | "name": "spender", 19 | "type": "address" 20 | }, 21 | { 22 | "indexed": false, 23 | "internalType": "uint256", 24 | "name": "amount", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "Approval", 29 | "type": "event" 30 | }, 31 | { 32 | "anonymous": false, 33 | "inputs": [ 34 | { 35 | "indexed": true, 36 | "internalType": "address", 37 | "name": "previousController", 38 | "type": "address" 39 | }, 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "newController", 44 | "type": "address" 45 | } 46 | ], 47 | "name": "ControlTransferred", 48 | "type": "event" 49 | }, 50 | { 51 | "anonymous": false, 52 | "inputs": [ 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "previousOwner", 57 | "type": "address" 58 | }, 59 | { 60 | "indexed": true, 61 | "internalType": "address", 62 | "name": "newOwner", 63 | "type": "address" 64 | } 65 | ], 66 | "name": "OwnershipTransferred", 67 | "type": "event" 68 | }, 69 | { 70 | "anonymous": false, 71 | "inputs": [ 72 | { 73 | "indexed": true, 74 | "internalType": "address", 75 | "name": "from", 76 | "type": "address" 77 | }, 78 | { 79 | "indexed": true, 80 | "internalType": "address", 81 | "name": "to", 82 | "type": "address" 83 | }, 84 | { 85 | "indexed": false, 86 | "internalType": "uint256", 87 | "name": "amount", 88 | "type": "uint256" 89 | } 90 | ], 91 | "name": "Transfer", 92 | "type": "event" 93 | }, 94 | { 95 | "inputs": [], 96 | "name": "DOMAIN_SEPARATOR", 97 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 98 | "stateMutability": "view", 99 | "type": "function" 100 | }, 101 | { 102 | "inputs": [], 103 | "name": "PERMIT_TYPEHASH", 104 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "inputs": [ 110 | { "internalType": "address", "name": "", "type": "address" }, 111 | { "internalType": "address", "name": "", "type": "address" } 112 | ], 113 | "name": "allowance", 114 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 115 | "stateMutability": "view", 116 | "type": "function" 117 | }, 118 | { 119 | "inputs": [ 120 | { "internalType": "address", "name": "spender", "type": "address" }, 121 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 122 | ], 123 | "name": "approve", 124 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }], 130 | "name": "balanceOf", 131 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 132 | "stateMutability": "view", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [], 137 | "name": "controller", 138 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 139 | "stateMutability": "view", 140 | "type": "function" 141 | }, 142 | { 143 | "inputs": [], 144 | "name": "decimals", 145 | "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], 146 | "stateMutability": "view", 147 | "type": "function" 148 | }, 149 | { 150 | "inputs": [ 151 | { "internalType": "address", "name": "to", "type": "address" }, 152 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 153 | ], 154 | "name": "mint", 155 | "outputs": [], 156 | "stateMutability": "nonpayable", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [], 161 | "name": "name", 162 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 163 | "stateMutability": "view", 164 | "type": "function" 165 | }, 166 | { 167 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }], 168 | "name": "nonces", 169 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "inputs": [], 175 | "name": "owner", 176 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 177 | "stateMutability": "view", 178 | "type": "function" 179 | }, 180 | { 181 | "inputs": [ 182 | { "internalType": "address", "name": "owner", "type": "address" }, 183 | { "internalType": "address", "name": "spender", "type": "address" }, 184 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 185 | { "internalType": "uint256", "name": "deadline", "type": "uint256" }, 186 | { "internalType": "uint8", "name": "v", "type": "uint8" }, 187 | { "internalType": "bytes32", "name": "r", "type": "bytes32" }, 188 | { "internalType": "bytes32", "name": "s", "type": "bytes32" } 189 | ], 190 | "name": "permit", 191 | "outputs": [], 192 | "stateMutability": "nonpayable", 193 | "type": "function" 194 | }, 195 | { 196 | "inputs": [], 197 | "name": "removeControl", 198 | "outputs": [], 199 | "stateMutability": "nonpayable", 200 | "type": "function" 201 | }, 202 | { 203 | "inputs": [], 204 | "name": "renounceOwnership", 205 | "outputs": [], 206 | "stateMutability": "nonpayable", 207 | "type": "function" 208 | }, 209 | { 210 | "inputs": [], 211 | "name": "symbol", 212 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 213 | "stateMutability": "view", 214 | "type": "function" 215 | }, 216 | { 217 | "inputs": [], 218 | "name": "totalSupply", 219 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 220 | "stateMutability": "view", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { "internalType": "address", "name": "to", "type": "address" }, 226 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 227 | ], 228 | "name": "transfer", 229 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 230 | "stateMutability": "nonpayable", 231 | "type": "function" 232 | }, 233 | { 234 | "inputs": [ 235 | { 236 | "internalType": "address", 237 | "name": "newController", 238 | "type": "address" 239 | } 240 | ], 241 | "name": "transferControl", 242 | "outputs": [], 243 | "stateMutability": "nonpayable", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { "internalType": "address", "name": "from", "type": "address" }, 249 | { "internalType": "address", "name": "to", "type": "address" }, 250 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 251 | ], 252 | "name": "transferFrom", 253 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 254 | "stateMutability": "nonpayable", 255 | "type": "function" 256 | }, 257 | { 258 | "inputs": [ 259 | { "internalType": "address", "name": "newOwner", "type": "address" } 260 | ], 261 | "name": "transferOwnership", 262 | "outputs": [], 263 | "stateMutability": "nonpayable", 264 | "type": "function" 265 | } 266 | ] 267 | } 268 | -------------------------------------------------------------------------------- /ui/pages/join.tsx: -------------------------------------------------------------------------------- 1 | import { LockClosedIcon, SparklesIcon } from '@heroicons/react/24/outline' 2 | import { ethers } from 'ethers' 3 | import Link from 'next/link' 4 | import { useRouter } from 'next/router' 5 | import { useEffect, useMemo, useState } from 'react' 6 | import { useWaitForTransaction } from 'wagmi' 7 | import { 8 | nationToken, 9 | balancerDomain, 10 | nationPassportAgreementStatement, 11 | nationPassportAgreementURI, 12 | } from '../lib/config' 13 | import { useNationBalance } from '../lib/nation-token' 14 | import { NumberType, transformNumber } from '../lib/numbers' 15 | import { 16 | useClaimPassport, 17 | useHasPassport, 18 | useClaimRequiredBalance, 19 | } from '../lib/passport-nft' 20 | import { storeSignature, useSignAgreement } from '../lib/sign-agreement' 21 | import { useAccount } from '../lib/use-wagmi' 22 | import { useVeNationBalance } from '../lib/ve-token' 23 | import ActionButton from '../components/ActionButton' 24 | import Balance from '../components/Balance' 25 | import Confetti from '../components/Confetti' 26 | import GradientLink from '../components/GradientLink' 27 | import Head from '../components/Head' 28 | import MainCard from '../components/MainCard' 29 | 30 | export default function Join() { 31 | const { address } = useAccount() 32 | const { data: nationBalance, isLoading: nationBalanceLoading } = 33 | useNationBalance(address) 34 | const { data: veNationBalance, isLoading: veNationBalanceLoading } = 35 | useVeNationBalance(address) 36 | const { hasPassport, isLoading: hasPassportLoading } = useHasPassport(address) 37 | const { data: claimRequiredBalance, isLoading: claimRequiredBalanceLoading } = useClaimRequiredBalance() 38 | const requiredBalance = useMemo(() => { 39 | if (claimRequiredBalanceLoading) { 40 | return -1 41 | } 42 | return transformNumber(claimRequiredBalance, NumberType.string, 0) as number 43 | }, [claimRequiredBalance, claimRequiredBalanceLoading]) 44 | 45 | const { writeAsync: claim, data: claimData } = useClaimPassport() 46 | const { isLoading: claimPassportLoading } = useWaitForTransaction({ 47 | hash: claimData?.hash, 48 | }) 49 | const { isLoading: signatureLoading, signTypedData } = useSignAgreement({ 50 | onSuccess: async (signature: string) => { 51 | const sigs = ethers.utils.splitSignature(signature) 52 | const tx = await claim({ 53 | recklesslySetUnpreparedArgs: [sigs.v, sigs.r, sigs.s], 54 | }) 55 | 56 | // The signature will be stored permanently on the Ethereum blockchain, 57 | // so uploading it to IPFS is only a nice to have 58 | await storeSignature(signature, tx.hash) 59 | }, 60 | }) 61 | 62 | const signAndClaim = { 63 | isLoadingOverride: signatureLoading || claimPassportLoading, 64 | writeAsync: signTypedData, 65 | } 66 | 67 | const router = useRouter() 68 | 69 | const changeUrl = () => { 70 | router.replace('/join?mintingPassport=1', undefined, { shallow: true }) 71 | } 72 | 73 | useEffect(() => { 74 | if (hasPassport) { 75 | setTimeout(() => { 76 | router.push('/citizen') 77 | }, 5000) 78 | } 79 | }, [hasPassport, hasPassportLoading, router]) 80 | 81 | const [action, setAction] = useState({ 82 | mint: transformNumber(0, NumberType.bignumber), 83 | lockAndMint: transformNumber(0, NumberType.bignumber), 84 | }) 85 | 86 | useEffect(() => { 87 | if (!nationBalance || !veNationBalance) return 88 | setAction({ 89 | mint: veNationBalance.value.gte( 90 | transformNumber(claimRequiredBalance as number, NumberType.bignumber), 91 | ), 92 | lockAndMint: nationBalance.value 93 | .mul(4) 94 | .gte( 95 | transformNumber( 96 | (claimRequiredBalance as number) / 4, 97 | NumberType.bignumber, 98 | ), 99 | ), 100 | }) 101 | }, [ 102 | nationBalance, 103 | nationBalanceLoading, 104 | veNationBalance, 105 | veNationBalanceLoading, 106 | claimRequiredBalance, 107 | ]) 108 | 109 | return ( 110 | <> 111 | 112 | {hasPassport && } 113 | 114 |
    115 |
  • 118 | Lock $NATION 119 |
  • 120 |
  • 125 | Claim passport 126 |
  • 127 |
  • 130 | Adore your passport 131 |
  • 132 |
133 | 134 | {!hasPassport ? ( 135 | <> 136 |

137 | To become a citizen, you need to mint a passport NFT by holding at 138 | least{' '} 139 | 140 | {requiredBalance == -1 ? '...' : requiredBalance} $veNATION 141 | 142 | . This is to make sure all citizens are economically aligned. 143 |
144 |
145 | Your $NATION won't be taken away from you. As your lock matures, 146 | you can either withdraw your tokens or increase the lock time to 147 | keep citizenship. Passport NFTs represent membership and are 148 | currently not transferable. 149 |
150 |
151 | {nationPassportAgreementStatement}:{' '} 152 | 156 |

157 | 158 |
159 |
160 |
161 | 162 |
163 |
164 | Needed balance 165 |
166 |
167 | {requiredBalance == -1 ? '...' : requiredBalance} 168 |
169 |
$veNATION
170 |
171 | 172 |
173 |
174 | 175 |
176 |
177 | Your balance 178 |
179 |
180 | 185 |
186 |
$veNATION
187 |
188 |
189 | 190 | {action.mint ? ( 191 | 196 | Claim 197 | 198 | ) : action.lockAndMint ? ( 199 | <> 200 | 201 | 204 | 205 | 206 | ) : ( 207 | 213 | Buy $NATION 214 | 215 | )} 216 | 217 | ) : ( 218 | <> 219 |

220 | We are delighted to welcome you to Nation3 as a fellow citizen. 221 | You will be taken to your passport in a few seconds ✨ 222 |

223 |
224 | 225 |
226 | 227 | )} 228 |
229 | 230 | ) 231 | } 232 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 13 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 14 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 15 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 16 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 17 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 18 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 19 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 20 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 21 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 22 | /* Modules */ 23 | "module": "commonjs" /* Specify what module code is generated. */, 24 | // "rootDir": "./", /* Specify the root folder within your source files. */ 25 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 26 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 27 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 28 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 29 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 30 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 31 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 32 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 33 | /* JavaScript Support */ 34 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 35 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 36 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 37 | /* Emit */ 38 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 39 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 40 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 41 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 42 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 43 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 44 | // "removeComments": true, /* Disable emitting comments. */ 45 | // "noEmit": true, /* Disable emitting files from a compilation. */ 46 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 47 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 48 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 49 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 50 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 52 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 53 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 54 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 55 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 56 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 57 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 58 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 59 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 60 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 61 | /* Interop Constraints */ 62 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 63 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 64 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 65 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */, 66 | "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 67 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 68 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 69 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 70 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 71 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 72 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 73 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 74 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 75 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 76 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 77 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 78 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 79 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 80 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 81 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 82 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 83 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 84 | /* Completeness */ 85 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 86 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 87 | "lib": ["dom", "dom.iterable", "esnext"], 88 | "allowJs": true, 89 | "noEmit": true, 90 | "incremental": true, 91 | "moduleResolution": "node", 92 | "resolveJsonModule": true, 93 | "isolatedModules": true, 94 | "jsx": "preserve" 95 | }, 96 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 97 | "exclude": ["node_modules"] 98 | } 99 | -------------------------------------------------------------------------------- /ui/public/passport/art2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /ui/public/icons/connectors/walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/pages/liquidity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowTrendingUpIcon, 3 | CurrencyDollarIcon, 4 | SparklesIcon, 5 | CalculatorIcon, 6 | InformationCircleIcon, 7 | } from '@heroicons/react/24/outline' 8 | import { useEffect, useState } from 'react' 9 | import React from 'react' 10 | import { useBalancerPool } from '../lib/balancer' 11 | import { 12 | balancerPoolId, 13 | balancerLPToken, 14 | lpRewardsContract, 15 | // veNationRewardsMultiplier, 16 | balancerDomain, 17 | } from '../lib/config' 18 | import { 19 | useLiquidityRewards, 20 | usePoolTokenBalance, 21 | useVeNationBoost, 22 | useBoostedAPY, 23 | useDeposit, 24 | useWithdraw, 25 | useWithdrawAndClaim, 26 | useClaimRewards, 27 | } from '../lib/liquidity-rewards' 28 | import { NumberType, transformNumber } from '../lib/numbers' 29 | import { useAccount } from '../lib/use-wagmi' 30 | import { useVeNationBalance, useVeNationSupply } from '../lib/ve-token' 31 | import ActionButton from '../components/ActionButton' 32 | import Balance from '../components/Balance' 33 | import EthersInput from '../components/EthersInput' 34 | import GradientLink from '../components/GradientLink' 35 | import Head from '../components/Head' 36 | import MainCard from '../components/MainCard' 37 | 38 | export default function Liquidity() { 39 | const { address } = useAccount() 40 | 41 | const { data: veNationBalance, isLoading: veNationBalanceLoading } = 42 | useVeNationBalance(address) 43 | const { 44 | poolValue, 45 | nationPrice, 46 | isLoading: poolLoading, 47 | } = useBalancerPool(balancerPoolId) 48 | 49 | const { data: poolTokenBalance, isLoading: poolTokenBalanceLoading } = 50 | usePoolTokenBalance(address) 51 | 52 | const { 53 | liquidityRewardsAPY, 54 | unclaimedRewards, 55 | userDeposit, 56 | totalDeposit, 57 | userBalance, 58 | loading: liquidityRewardsLoading, 59 | } = useLiquidityRewards({ 60 | nationPrice, 61 | poolValue, 62 | address: address, 63 | }) 64 | 65 | const { data: veNationSupply } = useVeNationSupply() 66 | 67 | const { currentBoost, potentialBoost, canBoost } = useVeNationBoost({ 68 | userDeposit, 69 | totalDeposit, 70 | userVeNation: veNationBalance?.value, 71 | totalVeNation: veNationSupply, 72 | userBalance, 73 | }) 74 | 75 | const boostedAPY = useBoostedAPY({ 76 | defaultAPY: liquidityRewardsAPY, 77 | boostMultiplier: currentBoost, 78 | }) 79 | 80 | const [depositValue, setDepositValue] = useState(0) 81 | const [withdrawalValue, setWithdrawalValue] = useState('0') 82 | const deposit = useDeposit( 83 | transformNumber(depositValue, NumberType.bignumber) 84 | ) 85 | const withdraw = useWithdraw( 86 | transformNumber(withdrawalValue, NumberType.bignumber) 87 | ) 88 | 89 | // @ts-expect-error 90 | const claimRewards = useClaimRewards(unclaimedRewards) 91 | const withdrawAndClaimRewards = useWithdrawAndClaim() 92 | const [activeTab, setActiveTab] = useState(0) 93 | 94 | return ( 95 | <> 96 | 97 | 98 | 99 |

100 | Provide liquidity in the pool and then deposit the pool token here.{' '} 101 | 106 |
107 | {/* Get up to {veNationRewardsMultiplier}x more rewards with $veNATION.{' '} 108 | */} 109 |

110 | 111 |
112 |
113 |
114 | 115 |
116 | 117 |
118 | Total liquidity 119 |
120 | 121 |
122 | 132 |
133 |
134 | 135 |
136 |
137 | 138 |
139 | 140 |
Rewards APY
141 | 142 |
143 | 149 |
150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 |
158 | 159 |
Your veNATION
160 | 161 |
162 | 167 |
168 |
169 | 170 |
171 |
172 | 173 |
174 | 175 |
176 | Your boosted APY 177 |
178 | 179 |
180 | 185 |
186 |
187 |
188 | {canBoost && ( 189 |
190 |
191 | 192 | 193 |
194 | You can boost your APY to{' '} 195 | 196 | {transformNumber( 197 | ( 198 | ((transformNumber( 199 | liquidityRewardsAPY ?? 0, 200 | NumberType.number, 201 | ) as number) / 202 | 10 ** 18) * 203 | potentialBoost 204 | ).toString(), 205 | NumberType.number, 206 | 2, 207 | ) + '%'} 208 | 209 | . To do so, claim your current rewards. 210 |
211 |
212 |
213 | )} 214 | 215 |
216 |
217 |
218 | 222 | Claim 223 | 224 |
225 | 226 |
Your rewards
227 | 228 |
229 | 230 |
231 | 232 |
NATION tokens
233 |
234 |
235 | 236 |
237 |
238 | 255 | 256 |
257 | {activeTab === 0 ? ( 258 | <> 259 |

260 | Available to deposit:{' '} 261 | {' '} 265 | LP tokens 266 |

267 | 268 |
269 | 277 | 278 | 287 |
288 | 289 |
290 | 300 | Deposit 301 | 302 |
303 | 304 | ) : ( 305 | <> 306 |

307 | Available to withdraw:{' '} 308 | LP tokens 309 |

310 | 311 |
312 | { 319 | setWithdrawalValue(value) 320 | }} 321 | /> 322 | 323 | 337 |
338 | 339 |
340 | 344 | Withdraw 345 | 346 | 347 | 351 | userDeposit && 352 | setWithdrawalValue( 353 | transformNumber( 354 | userDeposit, 355 | NumberType.string, 356 | ) as string, 357 | ) 358 | } 359 | > 360 | Withdraw all and claim 361 |
365 | 366 |
367 |
368 |
369 | 370 | )} 371 |
372 |
373 |
374 |
375 | 376 | ) 377 | } -------------------------------------------------------------------------------- /ui/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowRightOnRectangleIcon, 3 | ArrowTopRightOnSquareIcon, 4 | BanknotesIcon, 5 | Bars3Icon, 6 | ChevronDownIcon, 7 | ChevronRightIcon, 8 | CurrencyDollarIcon, 9 | HomeIcon, 10 | KeyIcon, 11 | LockClosedIcon, 12 | NewspaperIcon, 13 | PlusIcon, 14 | Squares2X2Icon, 15 | UserIcon, 16 | UserPlusIcon, 17 | UsersIcon, 18 | XCircleIcon, 19 | } from '@heroicons/react/24/outline' 20 | import Image from 'next/image' 21 | import Link from 'next/link' 22 | import { useRouter } from 'next/router' 23 | import { useState } from 'react' 24 | // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message 25 | import Blockies from 'react-blockies' 26 | import { useConnect, useDisconnect, useEnsName } from 'wagmi' 27 | import { balancerDomain, etherscanDomain, nationToken } from '../lib/config' 28 | import { connectorIcons } from '../lib/connectors' 29 | import { useAccount } from '../lib/use-wagmi' 30 | import Logo from '../public/logo.svg' 31 | import ErrorCard from './ErrorCard' 32 | import { useErrorContext } from './ErrorProvider' 33 | import PassportCheck from './PassportCheck' 34 | import PreferredNetworkWrapper from './PreferredNetworkWrapper' 35 | 36 | type Indexable = { 37 | [key: string]: any 38 | } 39 | 40 | const navigation = [ 41 | { 42 | name: 'Start', 43 | href: '/', 44 | icon: , 45 | }, 46 | { 47 | name: 'Become a citizen', 48 | href: '/join', 49 | icon: , 50 | }, 51 | { 52 | name: 'Citizen directory', 53 | href: 'https://citizens.nation3.org', 54 | icon: , 55 | }, 56 | { 57 | name: 'Lock tokens', 58 | href: '/lock', 59 | icon: , 60 | }, 61 | { 62 | name: 'Liquidity rewards', 63 | href: '/liquidity', 64 | icon: , 65 | }, 66 | { 67 | name: 'Claim basic income', 68 | href: 'https://income.nation3.org', 69 | icon: , 70 | }, 71 | { 72 | name: 'Buy $NATION', 73 | href: `${balancerDomain}/swap/ether/${nationToken}`, 74 | icon: , 75 | }, 76 | { 77 | name: 'Homepage', 78 | href: 'https://nation3.org', 79 | icon: , 80 | }, 81 | { 82 | name: 'Wiki', 83 | href: 'https://wiki.nation3.org', 84 | icon: , 85 | }, 86 | ] 87 | 88 | export default function Layout({ children }: any) { 89 | const router = useRouter() 90 | const { 91 | connectors, 92 | connect, 93 | error: connectError, 94 | data: connectData, 95 | } = useConnect() 96 | const { address } = useAccount() 97 | 98 | const { data: ensName } = useEnsName({ address: address ?? '' }) 99 | const { disconnect } = useDisconnect() 100 | const [nav, setNav] = useState(navigation) 101 | const errorContext = useErrorContext() 102 | 103 | const onPassportChecked = (hasPassport: boolean) => { 104 | if (hasPassport) { 105 | navigation[1].name = 'Welcome, citizen' 106 | navigation[1].href = '/citizen' 107 | setNav(navigation) 108 | if (router.pathname === '/join' && !router.query.mintingPassport) { 109 | router.push('/citizen') 110 | } 111 | } else { 112 | if (router.pathname === '/citizen') { 113 | router.push('/join') 114 | } 115 | } 116 | } 117 | 118 | const layout = ( 119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 | Logo 128 | 129 |
130 |
131 | 137 |
138 |
139 |
140 |
141 |
142 | 143 |
144 | 145 |
146 |
147 | 148 |
149 |
{children}
150 |
151 |
152 |
153 |
154 |
155 | 159 |
160 |
161 |
162 | 163 | 164 | Logo 165 | 166 | {/* Logo placeholder for Dark Mode */} 167 | {/* 168 | 169 | */} 170 | 171 |
172 |
173 |
    174 | {nav.map((item: any) => ( 175 |
  • 178 | // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. 179 | (document.getElementById('side-drawer').checked = false) 180 | } 181 | key={item.href} 182 | > 183 | {item.href.charAt(0) === '/' ? ( 184 | 188 | {item.icon} 189 | {item.name} 190 | 191 | 192 | ) : ( 193 | 201 | {item.icon} 202 | {item.name} 203 | 204 | 205 | )} 206 |
  • 207 | ))} 208 |
209 |
    210 | {address ? ( 211 |
  • 212 | 225 |
  • 226 | ) : ( 227 |
  • 228 | 234 |
  • 235 | )} 236 |
237 |
238 |
239 |
240 |
241 | 242 | 243 | 244 | 366 | {errorContext?.errors ? ( 367 |
368 |
369 | {errorContext.errors.map((error: any) => ( 370 | 371 | ))} 372 |
373 |
374 | ) : ( 375 | '' 376 | )} 377 |
378 | ) 379 | 380 | if (address) { 381 | return ( 382 | 383 | {layout} 384 | 385 | ) 386 | } else { 387 | return layout 388 | } 389 | } 390 | --------------------------------------------------------------------------------