├── contracts └── deployments │ ├── .gitkeep │ ├── sepolia.json │ ├── goerli.json │ └── mainnet.json ├── .github ├── CODEOWNERS ├── codecov.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ui_mainnet.yml │ └── ui_sepolia.yml ├── 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.ts │ ├── date.test.ts │ ├── use-handle-error.ts │ ├── network-id.test.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 │ ├── connectors.ts │ ├── static-call.ts │ ├── balancer.ts │ ├── passport-nft.ts │ ├── config.ts │ ├── ve-token.ts │ └── liquidity-rewards.ts ├── cypress.config.ts ├── .env.goerli ├── next-env.d.ts ├── .eslintrc ├── jest.config.js ├── .prettierrc.json ├── components │ ├── EthersInput.tsx │ ├── Confetti.tsx │ ├── PassportCheck.tsx │ ├── PassportExpiration.tsx │ ├── PreferredNetworkWrapper.tsx │ ├── Balance.tsx │ ├── HomeCard.tsx │ ├── ErrorCard.tsx │ ├── GradientLink.tsx │ ├── Head.tsx │ ├── MainCard.tsx │ ├── TimeRange.tsx │ ├── SwitchNetworkBanner.tsx │ ├── ErrorProvider.tsx │ ├── ActionButton.tsx │ ├── ActionNeedsTokenApproval.tsx │ └── Layout.tsx ├── styles │ ├── globals.css │ └── Home.module.css ├── .gitignore ├── cypress │ └── e2e │ │ ├── lock.cy.ts │ │ └── app.cy.ts ├── pages │ ├── _document.tsx │ ├── api │ │ └── store-signature.ts │ ├── _app.tsx │ ├── liquidity.tsx │ ├── index.tsx │ ├── citizen.tsx │ ├── join.tsx │ └── lock.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/artemd24/citizen-app/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/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/artemd24/citizen-app/HEAD/ui/public/passport/art3.png -------------------------------------------------------------------------------- /ui/public/passport/art4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/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/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff -------------------------------------------------------------------------------- /ui/public/fonts/Bossa-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff -------------------------------------------------------------------------------- /ui/public/fonts/UniversalSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemd24/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:42069', 6 | supportFile: false 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /ui/.env.goerli: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN=goerli 2 | INFURA_ID= 3 | ALCHEMY_ID= 4 | ETHERSCAN_ID= 5 | NFTSTORAGE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDRlOTU2NDk3NjI4ZTdGMEQyODQxZWZkNEIwQ -------------------------------------------------------------------------------- /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/.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/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 'goerli': 11 | return 5 12 | case 'sepolia': 13 | return 11155111 14 | case 'local': 15 | return 31337 16 | default: 17 | return 1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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/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/lib/network-id.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals' 2 | import { networkToId } from './network-id' 3 | 4 | test("networkToId goerli", () => { 5 | const actual = networkToId('goerli') 6 | expect(actual).toBe(5) 7 | }) 8 | 9 | test("networkToId sepolia", () => { 10 | const actual = networkToId('sepolia') 11 | expect(actual).toBe(11155111) 12 | }) 13 | 14 | test("networkToId mainnet", () => { 15 | const actual = networkToId('mainnet') 16 | expect(actual).toBe(1) 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 | -------------------------------------------------------------------------------- /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/.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 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # cypress 38 | cypress/screenshots 39 | cypress/videos 40 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /contracts/deployments/sepolia.json: -------------------------------------------------------------------------------- 1 | { 2 | "nationToken": "0x23Ca3002706b71a440860E3cf8ff64679A00C9d7", 3 | "veNationToken": "0x8100e77899C24b0F7B516153F84868f850C034BF", 4 | "balancerLPToken": "0x6417755C00d5c17DeC196Df00a6F151E448B1471", 5 | "lpRewardsContract": "0x5514cF24D2241Ecc6012306927eA8d74E052416D", 6 | "nationPassportNFT": "0x11f30642277A70Dab74C6fAF4170a8b340BE2f98", 7 | "nationPassportNFTIssuer": "0xdad32e13E73ce4155a181cA0D350Fee0f2596940", 8 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL", 9 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link" 10 | } 11 | -------------------------------------------------------------------------------- /ui/public/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Related GitHub Issue 4 | 5 | 6 | 7 | ## Screenshots (if appropriate): 8 | 9 | 10 | 11 | ## How Has This Been Tested? 12 | 13 | 14 | 15 | - [ ] Status checks pass (lint, build, test) 16 | - [ ] Works on Sepolia preview deployment 17 | - [ ] Works on Mainnet preview deployment 18 | 19 | ## Are There Admin Tasks? 20 | 21 | 22 | -------------------------------------------------------------------------------- /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/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 |

{title}

17 | 18 | {children} 19 | 20 | 21 |
22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | if (href?.charAt(0) === '/') { 6 | return ( 7 | 8 | 11 | {text} → 12 | 13 | 14 | ) 15 | } else { 16 | return ( 17 | 23 | {text} → 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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/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/goerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "nationToken": "0x333A4823466879eeF910A04D473505da62142069", 3 | "veNationToken": "0xF7deF1D2FBDA6B74beE7452fdf7894Da9201065d", 4 | "nationDropContracts": [ 5 | "0xCBbd5a8871E6Cf995305e58168cEaFB0be512c67" 6 | ], 7 | "balancerVault": "0xba12222222228d8ba445958a75a0704d566bf2c8", 8 | "balancerPoolId": "0x6f57329d43f3de9ff39d4424576db920b55060b30002000000000000000000d7", 9 | "balancerLPToken": "0x6f57329d43f3de9ff39d4424576db920b55060b3", 10 | "lpRewardsContract": "0x5e9a1c70391711408e064104292ace9d0b9763b6", 11 | "nationPassportNFT": "0x51F728c58697aFf9582cFDe3cBD00EC83E9ae7FC", 12 | "nationPassportNFTIssuer": "0x8c16926819AB30B8b29A8E23F5C230d164337093", 13 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL", 14 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link" 15 | } 16 | -------------------------------------------------------------------------------- /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": "0x4f1e79793fD5F5805b285C3F29379b8056A4476B", 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 | -------------------------------------------------------------------------------- /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 | } 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/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/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.goerli .env.local 24 | ``` 25 | or 26 | ``` 27 | cp .env.sepolia .env.local 28 | ``` 29 | 30 | 31 | Build: 32 | ``` 33 | yarn build 34 | ``` 35 | 36 | Lint: 37 | ``` 38 | yarn lint 39 | ``` 40 | 41 | Start the development server: 42 | ``` 43 | yarn dev 44 | ``` 45 | 46 | Then open http://localhost:42069 in a browser. 47 | 48 | ## Integration Testing 49 | 50 | Run the integration tests: 51 | ``` 52 | yarn cypress 53 | ``` 54 | 55 | Run the integration tests headlessly: 56 | ``` 57 | yarn cypress:headless 58 | ``` 59 | 60 | ## Unit Testing 61 | 62 | Run unit tests: 63 | ``` 64 | yarn test 65 | ``` 66 | 67 | ## Code Coverage 68 | 69 | [![codecov](https://codecov.io/gh/nation3/citizen-app/branch/main/graph/badge.svg)](https://codecov.io/gh/nation3/citizen-app) 70 | 71 | [![codecov](https://codecov.io/gh/nation3/citizen-app/graphs/icicle.svg)](https://codecov.io/gh/nation3/citizen-app) 72 | 73 | Run code coverage: 74 | ``` 75 | yarn test:coverage 76 | ``` 77 | -------------------------------------------------------------------------------- /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 | goerli: 5, 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 | Nation3 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/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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citizen-app", 3 | "private": true, 4 | "scripts": { 5 | "dev": "PORT=42069 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:42069 cypress", 12 | "e2e:headless": "start-server-and-test dev http://localhost:42069 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 | "@types/node": "^20.10.4", 20 | "@types/react-dom": "^18.0.4", 21 | "daisyui": "^2.13.5", 22 | "ethers": "^5.7.1", 23 | "next": "12.1.0", 24 | "nft.storage": "^7.1.1", 25 | "react": "17.0.2", 26 | "react-animated-3d-card": "^1.0.2", 27 | "react-blockies": "^1.4.1", 28 | "react-confetti": "^6.0.1", 29 | "react-dom": "17.0.2", 30 | "react-use": "^17.5.0", 31 | "typescript": "^4.7.4", 32 | "use-nft": "^0.12.0", 33 | "wagmi": "0.12.8" 34 | }, 35 | "devDependencies": { 36 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 37 | "@types/jest": "^28.1.8", 38 | "@types/react": "18.0.1", 39 | "autoprefixer": "^10.4.0", 40 | "cypress": "^10.7.0", 41 | "eslint": "8.21.0", 42 | "eslint-config-next": "12.0.7", 43 | "eslint-config-prettier": "^8.3.0", 44 | "jest": "^28.1.3", 45 | "postcss": "^8.4.5", 46 | "prettier": "^3.2.5", 47 | "start-server-and-test": "^1.14.0", 48 | "tailwindcss": "^3.1.8", 49 | "ts-jest": "^28.0.8" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/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/connectors.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { mainnet, goerli, configureChains } from 'wagmi' 3 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet' 4 | import { InjectedConnector } from 'wagmi/connectors/injected' 5 | import { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect' 6 | import CoinbaseWalletIcon from '../public/icons/connectors/coinbase.svg' 7 | import FrameIcon from '../public/icons/connectors/frame.svg' 8 | import MetaMaskIcon from '../public/icons/connectors/metamask.svg' 9 | import WalletConnectIcon from '../public/icons/connectors/walletconnect.svg' 10 | import { networkToId } from './network-id' 11 | 12 | const chains = [mainnet, goerli] 13 | 14 | export function provider() { 15 | if (process.env.NEXT_PUBLIC_CHAIN === 'local') { 16 | console.log('Provider: Connected to localhost provider') 17 | return new ethers.providers.JsonRpcProvider( 18 | 'http://127.0.0.1:7545', 19 | networkToId(process.env.NEXT_PUBLIC_CHAIN) 20 | ) 21 | } else { 22 | console.log( 23 | `Provider: Connected to the external provider on chain ${process.env.NEXT_PUBLIC_CHAIN}` 24 | ) 25 | return ethers.getDefaultProvider(process.env.NEXT_PUBLIC_CHAIN, { 26 | infura: process.env.NEXT_PUBLIC_INFURA_ID, 27 | alchemy: process.env.NEXT_PUBLIC_ALCHEMY_ID, 28 | quorum: 1, 29 | }); 30 | } 31 | } 32 | 33 | export const connectors = [ 34 | new InjectedConnector({ 35 | chains, 36 | options: { shimDisconnect: true }, 37 | }), 38 | new WalletConnectConnector({ 39 | chains, 40 | options: { 41 | showQrModal: true, 42 | projectId: 'de21254f0716238419606243642a9266', 43 | }, 44 | }), 45 | new CoinbaseWalletConnector({ 46 | chains, 47 | options: { 48 | appName: 'Nation3 app', 49 | }, 50 | }), 51 | ] 52 | 53 | export const connectorIcons = { 54 | Frame: FrameIcon, 55 | MetaMask: MetaMaskIcon, 56 | WalletConnect: WalletConnectIcon, 57 | 'Coinbase Wallet': CoinbaseWalletIcon, 58 | } -------------------------------------------------------------------------------- /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/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 || "goerli"; 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 | 69 | -------------------------------------------------------------------------------- /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: 1000000, 23 | increase_amount: 1000000, 24 | increase_unlock_time: 1000000, 25 | withdraw: 800000, 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 | { gasLimit: gasLimits.withdraw } 92 | ) 93 | } 94 | 95 | export function useVeNationSupply() { 96 | return useContractRead(contractParams, 'totalSupply') 97 | } 98 | -------------------------------------------------------------------------------- /ui/pages/liquidity.tsx: -------------------------------------------------------------------------------- 1 | import { InformationCircleIcon } from '@heroicons/react/24/outline' 2 | import React from 'react' 3 | import { useBalancerPool } from '../lib/balancer' 4 | import { balancerPoolId } from '../lib/config' 5 | import { 6 | useLiquidityRewards, 7 | useWithdrawAndClaim, 8 | } from '../lib/liquidity-rewards' 9 | import { useAccount } from '../lib/use-wagmi' 10 | import ActionButton from '../components/ActionButton' 11 | import Balance from '../components/Balance' 12 | import Head from '../components/Head' 13 | import MainCard from '../components/MainCard' 14 | 15 | export default function Liquidity() { 16 | const { address } = useAccount() 17 | 18 | const { 19 | poolValue, 20 | nationPrice, 21 | isLoading: poolLoading, 22 | } = useBalancerPool(balancerPoolId) 23 | 24 | const { 25 | unclaimedRewards, 26 | userDeposit, 27 | loading: liquidityRewardsLoading, 28 | } = useLiquidityRewards({ 29 | nationPrice, 30 | poolValue, 31 | address, 32 | }) 33 | 34 | const withdrawAndClaimRewards = useWithdrawAndClaim() 35 | 36 | return ( 37 | <> 38 | 39 | 40 | 41 |

Our liquidity rewards program is over.

42 |

43 | Please{' '} 44 | 45 | withdraw your LP tokens and rewards 46 | {' '} 47 | as soon as possible. 48 |

49 |
50 |
51 |
Your stake
52 | 53 |
54 | 55 |
56 | 57 |
LP tokens
58 |
59 |
60 |
Your rewards
61 | 62 |
63 | 64 |
65 | 66 |
NATION tokens
67 |
68 |
69 | 73 | Withdraw all and claim 74 |
78 | 79 |
80 |
81 |
82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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://user-images.githubusercontent.com/95955389/169034356-f1fdb540-d65b-4c1b-bd4d-21c76f7f8af3.png)](https://app.nation3.org) 14 | 15 | ## File Structure 16 | 17 | The code in this repository is structured into two main parts: 18 | 19 | ``` 20 | . 21 | ├── contracts # The smart contracts 22 | └── ui # The user interface (UI) for interacting with the smart contracts 23 | ``` 24 | 25 | ## Run the UI locally 26 | 27 | See [ui/README.md](ui/README.md) 28 | 29 | ## Testing against the Sepolia Ethereum testnet 30 | 31 | Add Sepolia testnet variables to your local development environment: 32 | ``` 33 | cp .env.sepolia .env.local 34 | ``` 35 | 36 | Start the development server: 37 | ``` 38 | yarn dev 39 | ``` 40 | 41 | Once you go to http://localhost:42069, you will see the message "Nation3 uses Goerli as its preferred network": 42 | 43 | > Screen Shot 2022-05-21 at 11 10 06 AM 44 | 45 | Solve this by switching to the _Goerli Test Network_ in MetaMask: 46 | 47 | > Screen Shot 2022-05-21 at 11 03 28 AM 48 | 49 | ## Testing against the Sepolia Ethereum testnet 50 | 51 | Add Sepolia testnet variables to your local development environment: 52 | ``` 53 | cp .env.sepolia .env.local 54 | ``` 55 | 56 | Start the development server: 57 | ``` 58 | yarn dev 59 | ``` 60 | 61 | Once you go to http://localhost:42069, you will see the message "Nation3 uses Sepolia as its preferred network": 62 | 63 | > Screenshot 2024-01-25 at 13 09 56 64 | 65 | Solve this by switching to the Sepolia Test Network in MetaMask: 66 | 67 | > Screenshot 2024-01-25 at 14 45 18 68 | 69 | ## Run the smart contracts locally 70 | 71 | Follow the instructions at [`contracts/README.md#local-setup`](https://github.com/nation3/citizen-app/blob/main/contracts/README.md#local-setup). 72 | 73 | Update the `NEXT_PUBLIC_CHAIN` variable in `.env.local` to match your local Ethereum [node](https://github.com/nation3/citizen-app/blob/main/contracts/README.md#running-a-node). 74 | 75 | Start the development server: 76 | ``` 77 | yarn dev 78 | ``` 79 | -------------------------------------------------------------------------------- /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 | 65 | 66 |
67 | 68 |
69 |
70 | 76 | 82 | 83 | 84 | Access gated channels 85 | 86 | 87 | 93 | 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 | setLiquidityRewardsAPY( 72 | totalRewards 73 | .mul(transformNumber(12 / months, NumberType.bignumber)) 74 | .mul(transformNumber(nationPrice, NumberType.bignumber, 2)) 75 | .div(poolValue.mul(totalDeposit).div(lpTokensSupply)) 76 | ) 77 | } 78 | }, [ 79 | poolValue, 80 | totalDeposit, 81 | lpTokensSupply, 82 | nationPrice, 83 | totalRewards, 84 | totalRewardsLoading, 85 | totalDepositLoading, 86 | lpTokensSupplyLoading, 87 | ]) 88 | 89 | return { 90 | liquidityRewardsAPY, 91 | unclaimedRewards, 92 | userDeposit, 93 | totalDeposit, 94 | userBalance, 95 | loading: 96 | totalRewardsLoading || 97 | unclaimedRewardsLoading || 98 | userDepositLoading || 99 | totalDepositLoading || 100 | userBalanceLoading, 101 | } 102 | } 103 | 104 | export function usePoolTokenBalance(address: any) { 105 | return useBalance({ 106 | address: address, 107 | token: balancerLPToken, 108 | watch: true, 109 | enabled: address, 110 | }) 111 | } 112 | 113 | // userDeposit = amount of LP tokens staked by user 114 | // totalDeposit = amount of LP tokens in rewards contract 115 | // userVotingPower = veNationBalance 116 | // totalVotingPower = veNATION supply 117 | export function useVeNationBoost({ 118 | userDeposit, 119 | totalDeposit, 120 | userVeNation, 121 | totalVeNation, 122 | userBalance, 123 | }: any) { 124 | const [boost, setBoost] = useState({ 125 | canBoost: false, 126 | }) 127 | useEffect(() => { 128 | if ( 129 | userDeposit && 130 | totalDeposit && 131 | userVeNation && 132 | totalVeNation && 133 | userBalance 134 | ) { 135 | const n = { 136 | userDeposit: parseFloat( 137 | transformNumber(userDeposit, NumberType.string) as string 138 | ), 139 | totalDeposit: parseFloat( 140 | transformNumber(totalDeposit, NumberType.string) as string 141 | ), 142 | userVeNation: parseFloat( 143 | transformNumber(userVeNation, NumberType.string) as string 144 | ), 145 | totalVeNation: parseFloat( 146 | transformNumber(totalVeNation, NumberType.string) as string 147 | ), 148 | userBalance: parseFloat( 149 | transformNumber(userBalance, NumberType.string) as string 150 | ), 151 | } 152 | 153 | const baseBalance = n.userDeposit * 0.4 154 | 155 | let boostedBalance = 156 | baseBalance + 157 | ((n.totalDeposit * n.userVeNation) / n.totalVeNation) * (60 / 100) 158 | 159 | boostedBalance = Math.min(boostedBalance, n.userDeposit) 160 | 161 | const potentialBoost = boostedBalance / baseBalance 162 | 163 | boostedBalance = Math.min(boostedBalance, n.userDeposit) 164 | 165 | const currentBoost = n.userBalance / baseBalance 166 | 167 | setBoost({ 168 | currentBoost: transformNumber( 169 | Math.max(currentBoost, 1), 170 | NumberType.bignumber 171 | ), 172 | potentialBoost: transformNumber(potentialBoost, NumberType.bignumber), 173 | canBoost: 174 | Math.trunc(potentialBoost * 10) > Math.trunc(currentBoost * 10), 175 | }) 176 | } 177 | }, [userDeposit, totalDeposit, userVeNation, totalVeNation, userBalance]) 178 | 179 | return boost 180 | } 181 | 182 | export function useBoostedAPY({ defaultAPY, boostMultiplier }: any) { 183 | const [apy, setAPY] = useState( 184 | parseFloat(transformNumber(defaultAPY, NumberType.string) as string) 185 | ) 186 | useEffect(() => { 187 | let defaultAPYasNumber = transformNumber( 188 | defaultAPY, 189 | NumberType.number 190 | ) as number 191 | let boostMultiplierAsNumber = transformNumber( 192 | boostMultiplier, 193 | NumberType.number 194 | ) as number 195 | 196 | if (defaultAPYasNumber != 0 && boostMultiplierAsNumber != 0) { 197 | setAPY(defaultAPYasNumber * boostMultiplierAsNumber) 198 | } 199 | }, [defaultAPY, boostMultiplier]) 200 | return apy 201 | } 202 | 203 | // Using Wagmi's contractWrite directly, getting a "no signer connected" error otherwise 204 | export function useClaimRewards() { 205 | return useContractWrite(contractParams, 'claimRewards', undefined, { gasLimit: 300000 }) 206 | } 207 | 208 | export function useDeposit(amount: any) { 209 | return useContractWrite(contractParams, 'deposit', [amount], { gasLimit: 300000 }) 210 | } 211 | 212 | export function useWithdraw(amount: any) { 213 | return useContractWrite(contractParams, 'withdraw', [amount], { gasLimit: 300000 }) 214 | } 215 | 216 | export function useWithdrawAndClaim() { 217 | return useContractWrite(contractParams, 'withdrawAndClaim', undefined, { gasLimit: 300000 }) 218 | } 219 | -------------------------------------------------------------------------------- /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 |
  • 116 | Lock $NATION 117 |
  • 118 |
  • 122 | Claim passport 123 |
  • 124 |
  • 125 | Adore your passport 126 |
  • 127 |
128 | 129 | {!hasPassport ? ( 130 | <> 131 |

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

150 | 151 |
152 |
153 |
154 | 155 |
156 |
Needed balance
157 |
{requiredBalance == -1 ? "..." : requiredBalance}
158 |
$veNATION
159 |
160 | 161 |
162 |
163 | 164 |
165 |
Your balance
166 |
167 | 172 |
173 |
$veNATION
174 |
175 |
176 | 177 | {action.mint ? ( 178 | 183 | Claim 184 | 185 | ) : action.lockAndMint ? ( 186 | <> 187 | 188 | 191 | 192 | 193 | ) : ( 194 | 200 | Buy $NATION 201 | 202 | )} 203 | 204 | ) : ( 205 | <> 206 |

207 | We are delighted to welcome you to Nation3 as a fellow citizen. 208 | You will be taken to your passport in a few seconds ✨ 209 |

210 |
211 | 212 |
213 | 214 | )} 215 |
216 | 217 | ) 218 | } 219 | -------------------------------------------------------------------------------- /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/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/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BanknotesIcon, 3 | ChevronDownIcon, 4 | ChevronRightIcon, 5 | CurrencyDollarIcon, 6 | ArrowTopRightOnSquareIcon, 7 | HomeIcon, 8 | KeyIcon, 9 | LockClosedIcon, 10 | ArrowRightOnRectangleIcon, 11 | Bars3Icon, 12 | NewspaperIcon, 13 | PlusIcon, 14 | UserPlusIcon, 15 | UserIcon, 16 | UsersIcon, 17 | Squares2X2Icon, 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 Script from 'next/script' 24 | import { useState } from 'react' 25 | // @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 26 | import Blockies from 'react-blockies' 27 | import { useConnect, useDisconnect, useEnsName } from 'wagmi' 28 | import { balancerDomain, etherscanDomain, nationToken } from '../lib/config' 29 | import { connectorIcons } from '../lib/connectors' 30 | import { useAccount } from '../lib/use-wagmi' 31 | import Logo from '../public/logo.svg' 32 | import ErrorCard from './ErrorCard' 33 | import { useErrorContext } from './ErrorProvider' 34 | import PassportCheck from './PassportCheck' 35 | import PreferredNetworkWrapper from './PreferredNetworkWrapper' 36 | 37 | type Indexable = { 38 | [key: string]: any 39 | } 40 | 41 | const navigation = [ 42 | { 43 | name: 'Start', 44 | href: '/', 45 | icon: , 46 | }, 47 | { 48 | name: 'Become a citizen', 49 | href: '/join', 50 | icon: , 51 | }, 52 | { 53 | name: 'Citizen directory', 54 | href: 'https://citizens.nation3.org', 55 | icon: , 56 | }, 57 | { 58 | name: 'Lock tokens', 59 | href: '/lock', 60 | icon: , 61 | }, 62 | { 63 | name: 'Liquidity rewards', 64 | href: '/liquidity', 65 | icon: , 66 | }, 67 | { 68 | name: 'Claim basic income', 69 | href: 'https://income.nation3.org', 70 | icon: , 71 | }, 72 | { 73 | name: 'Buy $NATION', 74 | href: `${balancerDomain}/swap/ether/${nationToken}`, 75 | icon: , 76 | }, 77 | { 78 | name: 'Homepage', 79 | href: 'https://nation3.org', 80 | icon: , 81 | }, 82 | { 83 | name: 'Wiki', 84 | href: 'https://wiki.nation3.org', 85 | icon: , 86 | }, 87 | ] 88 | 89 | export default function Layout({ children }: any) { 90 | const router = useRouter() 91 | const { 92 | connectors, 93 | connect, 94 | error: connectError, 95 | data: connectData, 96 | } = useConnect() 97 | const { address } = useAccount() 98 | 99 | const { data: ensName } = useEnsName({ address: address ?? '' }) 100 | const { disconnect } = useDisconnect() 101 | const [nav, setNav] = useState(navigation) 102 | const errorContext = useErrorContext() 103 | 104 | const onPassportChecked = (hasPassport: boolean) => { 105 | if (hasPassport) { 106 | navigation[1].name = 'Welcome, citizen' 107 | navigation[1].href = '/citizen' 108 | setNav(navigation) 109 | if (router.pathname === '/join' && !router.query.mintingPassport) { 110 | router.push('/citizen') 111 | } 112 | } else { 113 | if (router.pathname === '/citizen') { 114 | router.push('/join') 115 | } 116 | } 117 | } 118 | 119 | const layout = ( 120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 |
133 |
134 | 140 |
141 |
142 |
143 |
144 |
145 | 146 |
147 | 148 |
149 |
150 | 151 |
152 |
{children}
153 |
154 |
155 |
156 |
157 |
158 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 | 170 |
171 |
172 | 211 |
    212 | {address ? ( 213 |
  • 214 | 226 |
  • 227 | ) : ( 228 |
  • 229 | 235 |
  • 236 | )} 237 |
238 |
239 |
240 |
241 |
242 | 243 | 244 | 245 | 353 | {errorContext?.errors ? ( 354 |
355 |
356 | {errorContext.errors.map((error: any) => ( 357 | 358 | ))} 359 |
360 |
361 | ) : ( 362 | '' 363 | )} 364 |
365 | ) 366 | 367 | if (address) { 368 | return ( 369 | 370 | {layout} 371 | 372 | ) 373 | } else { 374 | return layout 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /ui/pages/lock.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ClockIcon, 3 | InformationCircleIcon, 4 | ExclamationTriangleIcon, 5 | LockClosedIcon, 6 | SparklesIcon, 7 | } from '@heroicons/react/24/outline' 8 | import { BigNumber, ethers } from 'ethers' 9 | import { useEffect, useMemo, useState } from 'react' 10 | import { nationToken, veNationToken } from '../lib/config' 11 | import { dateToReadable } from '../lib/date' 12 | import { useNationBalance } from '../lib/nation-token' 13 | import { NumberType, transformNumber } from '../lib/numbers' 14 | import { useAccount } from '../lib/use-wagmi' 15 | import { useClaimRequiredBalance } from '../lib/passport-nft' 16 | import { 17 | useVeNationBalance, 18 | useVeNationCreateLock, 19 | useVeNationIncreaseLock, 20 | useVeNationLock, 21 | useVeNationWithdrawLock, 22 | } from '../lib/ve-token' 23 | import ActionButton, { ActionButtonProps } from '../components/ActionButton' 24 | import Balance from '../components/Balance' 25 | import EthersInput from '../components/EthersInput' 26 | import GradientLink from '../components/GradientLink' 27 | import Head from '../components/Head' 28 | import MainCard from '../components/MainCard' 29 | import TimeRange from '../components/TimeRange' 30 | 31 | const bigNumberToDate = (bigNumber: any) => { 32 | return bigNumber && new Date(bigNumber.mul(1000).toNumber()) 33 | } 34 | 35 | const dateOut = (date: any, { days, years }: any) => { 36 | if (!date) return 37 | let dateOut = date 38 | days && dateOut.setDate(date.getDate() + days) 39 | years && dateOut.setFullYear(date.getFullYear() + years) 40 | return dateOut 41 | } 42 | 43 | const calculateVeNation = ({ 44 | nationAmount, 45 | veNationAmount, 46 | time, 47 | lockTime, 48 | max, 49 | }: any) => { 50 | if (!nationAmount) return 0 51 | 52 | const vestingStart = calculateVestingStart({ 53 | nationAmount, 54 | veNationAmount, 55 | lockTime, 56 | }) 57 | const percentage = (time - vestingStart) / (max - vestingStart) 58 | const finalVeNationAmount = nationAmount * percentage 59 | return finalVeNationAmount.toFixed(finalVeNationAmount > 1 ? 2 : 8) 60 | } 61 | 62 | const calculateVestingStart = ({ 63 | nationAmount, 64 | veNationAmount, 65 | lockTime, 66 | }: any) => { 67 | const fourYears = 31556926000 * 4 68 | return lockTime - (veNationAmount / nationAmount) * fourYears 69 | } 70 | 71 | export default function Lock() { 72 | const { address } = useAccount() 73 | 74 | const { data: nationBalance, isLoading: nationBalanceLoading } = 75 | useNationBalance(address) 76 | 77 | const { data: veNationBalance, isLoading: veNationBalanceLoading } = 78 | useVeNationBalance(address) 79 | 80 | const { data: claimRequiredBalance, isLoading: claimRequiredBalanceLoading } = useClaimRequiredBalance() 81 | const requiredBalance = useMemo(() => { 82 | if (claimRequiredBalanceLoading) { 83 | return -1 84 | } 85 | return transformNumber(claimRequiredBalance, NumberType.string, 0) as number 86 | }, [claimRequiredBalance, claimRequiredBalanceLoading]) 87 | 88 | const { data: veNationLock, isLoading: veNationLockLoading } = 89 | useVeNationLock(address) 90 | 91 | const [hasLock, setHasLock] = useState() 92 | useEffect(() => { 93 | !veNationLockLoading && setHasLock(veNationLock && veNationLock[0] != 0) 94 | // eslint-disable-next-line react-hooks/exhaustive-deps 95 | }, [veNationLock]) 96 | 97 | const [hasExpired, setHasExpired] = useState() 98 | useEffect(() => { 99 | !veNationLockLoading && 100 | setHasExpired( 101 | veNationLock && 102 | veNationLock[1] != 0 && 103 | ethers.BigNumber.from(Date.now()).gte(veNationLock[1].mul(1000)), 104 | ) 105 | // eslint-disable-next-line react-hooks/exhaustive-deps 106 | }, [veNationLock]) 107 | 108 | const [lockAmount, setLockAmount] = useState('') 109 | 110 | const oneWeekOut = useMemo(() => dateOut(new Date(), { days: 7 }), []) 111 | 112 | const [lockTime, setLockTime] = useState({ 113 | value: ethers.BigNumber.from(+oneWeekOut), 114 | formatted: dateToReadable(oneWeekOut), 115 | } as any) 116 | 117 | const [minMaxLockTime, setMinMaxLockTime] = useState({} as any) 118 | 119 | const [canIncrease, setCanIncrease] = useState({ amount: true, time: true }) 120 | const [wantsToIncrease, setWantsToIncrease] = useState(false) 121 | 122 | useEffect(() => { 123 | if (hasLock && veNationLock && !wantsToIncrease) { 124 | !lockAmount && setLockAmount(ethers.utils.formatEther(veNationLock[0])) 125 | const origTime = { 126 | value: veNationLock[1], 127 | formatted: dateToReadable(bigNumberToDate(veNationLock[1])), 128 | } 129 | !lockTime.orig && 130 | setLockTime({ 131 | ...origTime, 132 | orig: origTime, 133 | }) 134 | } 135 | // eslint-disable-next-line react-hooks/exhaustive-deps 136 | }, [hasLock, veNationLock]) 137 | 138 | useEffect(() => { 139 | if (hasLock && veNationLock) { 140 | const originalLockDate = dateToReadable(bigNumberToDate(veNationLock[1])) 141 | setMinMaxLockTime({ 142 | min: originalLockDate, 143 | max: dateToReadable(dateOut(new Date(), { years: 4 })), 144 | }) 145 | setCanIncrease({ 146 | amount: (lockAmount && 147 | ethers.utils.parseEther(lockAmount).gt(veNationLock[0])) as boolean, 148 | time: 149 | lockTime?.value && 150 | lockTime.value.gt( 151 | +dateOut(bigNumberToDate(veNationLock[1]), { days: 7 }), 152 | ), 153 | }) 154 | } else { 155 | setMinMaxLockTime({ 156 | min: dateToReadable(oneWeekOut), 157 | max: dateToReadable(dateOut(new Date(), { years: 4 })), 158 | }) 159 | } 160 | }, [hasLock, lockAmount, lockTime, veNationLock, oneWeekOut]) 161 | 162 | const createLock = useVeNationCreateLock( 163 | lockAmount && ethers.utils.parseEther(lockAmount), 164 | lockTime.value.div(1000), 165 | ) 166 | 167 | const increaseLock = useVeNationIncreaseLock({ 168 | currentAmount: veNationLock && veNationLock[0], 169 | newAmount: 170 | lockAmount && 171 | veNationLock && 172 | ethers.utils.parseEther(lockAmount).sub(veNationLock[0]), 173 | currentTime: veNationLock && veNationLock[1], 174 | newTime: lockTime?.value.div(1000), 175 | }) 176 | 177 | const withdraw = useVeNationWithdrawLock() 178 | 179 | const approval = useMemo( 180 | () => ({ 181 | token: nationToken, 182 | spender: veNationToken, 183 | amountNeeded: 184 | hasLock && veNationLock && veNationLock[0] 185 | ? ( 186 | transformNumber( 187 | lockAmount ?? '0', 188 | NumberType.bignumber, 189 | ) as BigNumber 190 | ).sub(veNationLock[0]) 191 | : transformNumber(lockAmount ?? '0', NumberType.bignumber), 192 | approveText: 'Approve $NATION', 193 | allowUnlimited: false, 194 | }), 195 | [hasLock, veNationLock, lockAmount], 196 | ) 197 | 198 | return ( 199 | <> 200 | 201 | 202 | 203 |

204 | $veNATION enables governance and minting passport NFTs.{' '} 205 | 211 |

212 | {!hasLock ? ( 213 | <> 214 |

215 | Your veNATION balance is dynamic and always correlates to the 216 | remainder of the time lock. As time passes and the remainder of 217 | time lock decreases, your veNATION balance decreases. If you want 218 | to increase it, you have to either increase the time lock or add 219 | more NATION. $NATION balance stays the same. 220 |
221 |
222 | 223 | {requiredBalance == -1 ? "..." : requiredBalance} $veNATION 224 | {' '} 225 | will be needed to mint a passport NFT. 226 |
227 |
228 | Some examples of how to get to {requiredBalance == -1 ? "..." : requiredBalance} $veNATION: 229 |

230 | 231 |
    232 |
  • At least {requiredBalance == -1 ? "..." : requiredBalance} $NATION locked for 4 years, or
  • 233 | 234 |
  • 235 | At least {requiredBalance == -1 ? "..." : requiredBalance * 2} $NATION locked for 2 years, or 236 |
  • 237 | 238 |
  • At least {requiredBalance == -1 ? "..." : requiredBalance * 4} $NATION locked for 1 year
  • 239 |
240 | 241 |
242 |
243 | 244 | 245 | We suggest you to obtain at least {(requiredBalance == -1 ? "..." : requiredBalance) || 0 + 0.5}{' '} 246 | $veNATION if you want to mint a passport NFT, since $veNATION 247 | balance drops over time. If it falls below the required 248 | threshold, your passport can be revoked. You can always lock 249 | more $NATION later. 250 | 251 |
252 |
253 | 254 | ) : ( 255 | '' 256 | )} 257 | {hasLock && ( 258 | <> 259 |
260 |
261 |
262 | 263 |
264 |
Your $veNATION
265 |
266 | 276 |
277 |
278 | 279 |
280 |
281 | 282 |
283 |
Your locked $NATION
284 |
285 | 290 |
291 |
292 |
293 | 294 |
295 |
296 |
297 | 298 |
299 |
Your lock expiration date
300 |
301 | {veNationLock && 302 | dateToReadable(bigNumberToDate(veNationLock[1]))} 303 |
304 |
305 |
306 | 307 | )} 308 | 309 |
310 |
311 |
312 | {!hasExpired ? ( 313 | <> 314 |

315 | Available to lock:{' '} 316 | {' '} 320 | $NATION 321 |

322 | 333 |
334 | { 346 | setLockAmount(value) 347 | setWantsToIncrease(true) 348 | }} 349 | /> 350 | 351 | 366 |
367 | 378 | { 386 | if (e.target.value < minMaxLockTime.min) { 387 | return false 388 | } 389 | setLockTime({ 390 | ...lockTime, 391 | formatted: e.target.value 392 | ? e.target.value 393 | : lockTime.orig.formatted, 394 | value: e.target.value 395 | ? ethers.BigNumber.from(Date.parse(e.target.value)) 396 | : lockTime.orig.value, 397 | }) 398 | setWantsToIncrease(!!e.target.value) 399 | }} 400 | /> 401 | 402 | { 408 | setLockTime({ 409 | ...lockTime, 410 | formatted: dateToReadable(newDate), 411 | value: ethers.BigNumber.from(Date.parse(newDate)), 412 | }) 413 | setWantsToIncrease(true) 414 | }} 415 | /> 416 | {wantsToIncrease ? ( 417 |

418 | Your final balance will be approx{' '} 419 | {calculateVeNation({ 420 | nationAmount: lockAmount && +lockAmount, 421 | veNationAmount: transformNumber( 422 | veNationBalance?.value || 0, 423 | NumberType.number, 424 | ), 425 | time: Date.parse(lockTime.formatted), 426 | lockTime: Date.parse(new Date().toString()), 427 | max: Date.parse(minMaxLockTime.max), 428 | })}{' '} 429 | $veNATION 430 |

431 | ) : ( 432 | '' 433 | )} 434 |
435 | 443 | {!hasLock 444 | ? 'Lock' 445 | : `Increase lock ${canIncrease.amount ? 'amount' : '' 446 | } ${canIncrease.amount && canIncrease.time ? '&' : '' 447 | } ${canIncrease.time ? 'time' : ''}`} 448 | 449 |
450 | 451 | ) : ( 452 | <> 453 |

Your previous lock has expired, you need to withdraw

454 | 455 |
456 | 460 | Withdraw 461 | 462 |
463 | 464 | )} 465 |
466 |
467 |
468 |
469 | 470 | ) 471 | } 472 | --------------------------------------------------------------------------------