├── contracts
└── deployments
│ ├── .gitkeep
│ ├── sepolia.json
│ └── mainnet.json
├── .github
├── CODEOWNERS
├── codecov.yml
├── dependabot.yml
├── workflows
│ ├── ui_mainnet.yml
│ └── ui_sepolia.yml
└── pull_request_template.md
├── ui
├── next.config.js
├── public
│ ├── favicon.ico
│ ├── social.jpg
│ ├── passport
│ │ ├── art3.png
│ │ ├── art4.png
│ │ ├── icon-plain.svg
│ │ ├── ballot.svg
│ │ ├── contactless.svg
│ │ ├── discord.svg
│ │ ├── logo-plain.svg
│ │ ├── art.svg
│ │ └── art2.svg
│ ├── fonts
│ │ ├── Bossa-Bold.woff
│ │ ├── Bossa-Black.woff
│ │ ├── Bossa-Black.woff2
│ │ ├── Bossa-Bold.woff2
│ │ ├── Bossa-Light.woff
│ │ ├── Bossa-Light.woff2
│ │ ├── Bossa-Medium.woff
│ │ ├── Bossa-Medium.woff2
│ │ ├── Bossa-Regular.woff
│ │ ├── Bossa-Regular.woff2
│ │ ├── UniversalSans-Italic.woff
│ │ ├── UniversalSans-Italic.woff2
│ │ ├── UniversalSans-Regular.woff
│ │ ├── UniversalSans-Regular.woff2
│ │ └── stylesheet.css
│ ├── icon.svg
│ ├── flag.svg
│ ├── icons
│ │ └── connectors
│ │ │ ├── frame.svg
│ │ │ ├── coinbase.svg
│ │ │ ├── metamask.svg
│ │ │ └── walletconnect.svg
│ └── logo.svg
├── .env.mainnet
├── .env.sepolia
├── postcss.config.js
├── lib
│ ├── date.ts
│ ├── nation-token.ts
│ ├── network-id.test.ts
│ ├── network-id.ts
│ ├── date.test.ts
│ ├── use-handle-error.ts
│ ├── use-preferred-network.ts
│ ├── approve.ts
│ ├── passport-expiration.ts
│ ├── passport-expiration-hook.ts
│ ├── passport-expiration.test.ts
│ ├── sign-agreement.ts
│ ├── numbers.test.ts
│ ├── use-wagmi.ts
│ ├── numbers.ts
│ ├── static-call.ts
│ ├── connectors.ts
│ ├── balancer.ts
│ ├── passport-nft.ts
│ ├── config.ts
│ ├── ve-token.ts
│ └── liquidity-rewards.ts
├── cypress.config.ts
├── next-env.d.ts
├── .eslintrc
├── jest.config.js
├── .prettierrc.json
├── components
│ ├── EthersInput.tsx
│ ├── GradientLink.tsx
│ ├── Confetti.tsx
│ ├── PassportCheck.tsx
│ ├── PassportExpiration.tsx
│ ├── PreferredNetworkWrapper.tsx
│ ├── Balance.tsx
│ ├── ErrorCard.tsx
│ ├── HomeCard.tsx
│ ├── Head.tsx
│ ├── TimeRange.tsx
│ ├── MainCard.tsx
│ ├── SwitchNetworkBanner.tsx
│ ├── ErrorProvider.tsx
│ ├── ActionButton.tsx
│ ├── ActionNeedsTokenApproval.tsx
│ └── Layout.tsx
├── styles
│ ├── globals.css
│ └── Home.module.css
├── cypress
│ └── e2e
│ │ ├── lock.cy.ts
│ │ ├── app.cy.ts
│ │ └── liquidity.cy.ts
├── .gitignore
├── pages
│ ├── _document.tsx
│ ├── api
│ │ └── store-signature.ts
│ ├── _app.tsx
│ ├── index.tsx
│ ├── citizen.tsx
│ ├── join.tsx
│ └── liquidity.tsx
├── tailwind.config.js
├── README.md
├── package.json
├── abis
│ └── ERC20.json
└── tsconfig.json
└── README.md
/contracts/deployments/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @nation3/developer-level-4
2 |
--------------------------------------------------------------------------------
/ui/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: 40..80
3 | round: down
4 | precision: 2
5 |
--------------------------------------------------------------------------------
/ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/favicon.ico
--------------------------------------------------------------------------------
/ui/public/social.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/social.jpg
--------------------------------------------------------------------------------
/ui/.env.mainnet:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHAIN=mainnet
2 | INFURA_ID=
3 | ALCHEMY_ID=
4 | ETHERSCAN_ID=
5 | NFTSTORAGE_KEY=
--------------------------------------------------------------------------------
/ui/public/passport/art3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/passport/art3.png
--------------------------------------------------------------------------------
/ui/public/passport/art4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/passport/art4.png
--------------------------------------------------------------------------------
/ui/.env.sepolia:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHAIN=sepolia
2 | INFURA_ID=
3 | ALCHEMY_ID=
4 | ETHERSCAN_ID=
5 | NFTSTORAGE_KEY=
6 |
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff
--------------------------------------------------------------------------------
/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nation3/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff2
--------------------------------------------------------------------------------
/ui/lib/date.ts:
--------------------------------------------------------------------------------
1 |
2 | export const dateToReadable = (date: Date | undefined): string | undefined => {
3 | if (!date) return undefined;
4 | return date.toISOString().substring(0, 10)
5 | }
6 |
--------------------------------------------------------------------------------
/ui/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | e2e: {
5 | baseUrl: 'http://localhost:3000',
6 | supportFile: false
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/ui/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/ui/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "prettier"],
3 | "rules": {
4 | "sort-imports": 0,
5 | "react/no-unescaped-entities": 0,
6 | "jsx-a11y/alt-text": [0],
7 | "react-hooks/exhaustive-deps": "error"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ui/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | coverageThreshold: {
6 | global: {
7 | lines: 11.00
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ui/lib/nation-token.ts:
--------------------------------------------------------------------------------
1 | import { nationToken } from '../lib/config'
2 | import { useBalance } from './use-wagmi'
3 |
4 | export function useNationBalance(address: any) {
5 | return useBalance({
6 | address: address,
7 | token: nationToken,
8 | watch: true,
9 | enabled: address,
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/ui/lib/network-id.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { networkToId } from './network-id'
3 |
4 |
5 | test("networkToId sepolia", () => {
6 | const actual = networkToId('sepolia')
7 | expect(actual).toBe(11155111)
8 | })
9 |
10 | test("networkToId mainnet", () => {
11 | const actual = networkToId('mainnet')
12 | expect(actual).toBe(1)
13 | })
14 |
--------------------------------------------------------------------------------
/ui/lib/network-id.ts:
--------------------------------------------------------------------------------
1 | export function networkToId(network: any) {
2 | if (network == undefined) {
3 | return 1
4 | }
5 | switch (network.toLowerCase()) {
6 | case 'mainnet':
7 | return 1
8 | case 'ethereum':
9 | return 1
10 | case 'sepolia':
11 | return 11155111
12 | case 'local':
13 | return 31337
14 | default:
15 | return 1
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 2,
4 | "semi": false,
5 | "bracketSpacing": true,
6 | "jsxBracketSameLine": false,
7 | "arrowParens": "always",
8 | "importOrder": [
9 | "^.*/lib/.*$",
10 | "^.*/components/.*$",
11 | "^.*/pages/(?!.*.css$).*$",
12 | "^[./](?!.*.css$).*$",
13 | "^.*(css)$"
14 | ],
15 | "importOrderSeparation": false
16 | }
17 |
--------------------------------------------------------------------------------
/ui/public/passport/icon-plain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm" # See documentation for possible values
7 | directory: "/ui" # Location of package manifests
8 | schedule:
9 | interval: "daily"
10 | open-pull-requests-limit: 1
11 |
--------------------------------------------------------------------------------
/ui/components/EthersInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { isFixedDecimalsNumber } from '../lib/numbers'
3 |
4 | export default function EthersInput({ onChange, ...props }: any) {
5 | const onInputChange = (e: any) => {
6 | const value = e.target.value
7 | const isValid = isFixedDecimalsNumber(value)
8 | if (isValid) {
9 | onChange(value)
10 | }
11 | }
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/ui/components/GradientLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import React from 'react'
3 |
4 | export default function GradientLink({ text, href, textSize }: any) {
5 | return (
6 |
11 | {text} →
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/ui/lib/date.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { BigNumber } from 'ethers'
3 | import { dateToReadable } from './date'
4 |
5 | test("dateToReadable - February", () => {
6 | const actual = dateToReadable(new Date(2022, 1, 15, 23, 59))
7 | expect(actual).toBe('2022-02-15')
8 | })
9 |
10 | test("dateToReadable - December", () => {
11 | const actual = dateToReadable(new Date(2024, 11, 31, 23, 59))
12 | expect(actual).toBe('2024-12-31')
13 | })
14 |
--------------------------------------------------------------------------------
/ui/lib/use-handle-error.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useErrorContext } from '../components/ErrorProvider'
3 |
4 | // For some contract interactions, a reverted call is not an error
5 | export function useHandleError(object: any, throwOnRevert = true) {
6 | const {addError} = useErrorContext()
7 | useEffect(() => {
8 | if (throwOnRevert && object.error) {
9 | addError([object.error])
10 | }
11 | }, [object.error, throwOnRevert, addError])
12 | return object
13 | }
14 |
--------------------------------------------------------------------------------
/ui/components/Confetti.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useState, useEffect } from 'react'
3 | import Confetti from 'react-confetti'
4 | import { useTimeout } from 'react-use'
5 |
6 | export default function ConfettiComponent() {
7 | const [width, setWidth] = useState(0)
8 | const [height, setHeight] = useState(0)
9 | const [isComplete] = useTimeout(5000)
10 |
11 | useEffect(() => {
12 | setWidth(window.innerWidth)
13 | setHeight(window.innerHeight)
14 | }, [])
15 |
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/ui/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url('/fonts/stylesheet.css');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | html,
8 | body {
9 | padding: 0;
10 | margin: 0;
11 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
12 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
13 | /* @apply font-body; */
14 | }
15 |
16 | a {
17 | color: inherit;
18 | text-decoration: none;
19 | }
20 |
21 | * {
22 | box-sizing: border-box;
23 | }
24 |
25 | .dark-card {
26 | @apply bg-slate-800 border border-slate-400;
27 | }
--------------------------------------------------------------------------------
/ui/cypress/e2e/lock.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Lock tokens', () => {
2 |
3 | it('lock amount: type more than 18 decimals', () => {
4 | cy.visit('/lock')
5 |
6 | // Expect empty value
7 | cy.get('#lockAmount')
8 | .invoke('val')
9 | .then(val => {
10 | expect(val).to.equal('')
11 | })
12 |
13 | // Type 19 decimals
14 | cy.get('#lockAmount').type('0.1919191919191919191')
15 |
16 | // Expect 18 decimals
17 | cy.get('#lockAmount')
18 | .invoke('val')
19 | .then(val => {
20 | expect(val).to.equal('0.191919191919191919')
21 | })
22 | })
23 | })
24 |
25 | export {}
26 |
--------------------------------------------------------------------------------
/ui/components/PassportCheck.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useHasPassport } from '../lib/passport-nft'
3 |
4 | export default function PassportCheck({
5 | children,
6 | address,
7 | onPassportChecked,
8 | }: React.PropsWithChildren<{
9 | address: string
10 | onPassportChecked: (hasPassport: boolean) => void
11 | }>) {
12 | const { hasPassport, isLoading: hasPassportLoading } = useHasPassport(address)
13 | useEffect(() => {
14 | if (!hasPassportLoading) {
15 | onPassportChecked(hasPassport)
16 | }
17 | }, [hasPassport, hasPassportLoading, onPassportChecked])
18 |
19 | return <>{children}>
20 | }
21 |
--------------------------------------------------------------------------------
/ui/components/PassportExpiration.tsx:
--------------------------------------------------------------------------------
1 | import { ClockIcon } from '@heroicons/react/24/outline'
2 | import { dateToReadable } from '../lib/date'
3 |
4 | interface Props {
5 | date: Date | undefined
6 | }
7 |
8 | export default function PassportExpiration({ date }: Props) {
9 | return (
10 |
11 |
12 |
13 |
14 |
Passport expiration date
15 |
16 | {!!date ? (date > new Date() ? dateToReadable(date) : 'Expired') : '-'}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/ui/components/PreferredNetworkWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import usePreferredNetwork from '../lib/use-preferred-network'
3 | import { useNetwork } from '../lib/use-wagmi'
4 | import SwitchNetworkBanner from './SwitchNetworkBanner'
5 |
6 | export default function PreferredNetworkWrapper({ children }: any) {
7 | const { chain: activeChain } = useNetwork()
8 | const { isPreferredNetwork, preferredNetwork } = usePreferredNetwork()
9 |
10 | return (
11 | <>
12 | {!isPreferredNetwork && activeChain?.id && (
13 |
14 | )}
15 | {children}
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/ui/components/Balance.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NumberType, transformNumber } from '../lib/numbers'
3 |
4 | export default function Balance({
5 | loading = false,
6 | balance,
7 | prefix = '',
8 | suffix = '',
9 | decimals = 2,
10 | }: any) {
11 | return (
12 | <>
13 | {loading ? (
14 |
15 | ) : balance ? (
16 | `${prefix}${transformNumber(
17 | balance,
18 | NumberType.string,
19 | decimals,
20 | )}${suffix}`
21 | ) : (
22 | 0
23 | )}
24 | >
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # yarn
28 | .yarn
29 |
30 | # local env files
31 | .env
32 | .env.local
33 | .env.development.local
34 | .env.test.local
35 | .env.production.local
36 |
37 | # vercel
38 | .vercel
39 |
40 | # cypress
41 | cypress/downloads
42 | cypress/screenshots
43 | cypress/videos
44 |
--------------------------------------------------------------------------------
/ui/cypress/e2e/app.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Nation3 App UI', () => {
2 |
3 | it('should load the index page', () => {
4 | cy.visit('/')
5 | cy.get('h1').contains('Welcome to Nation3')
6 | }),
7 |
8 | it('should load the /join page', () => {
9 | cy.visit('/join')
10 | cy.get('h2').contains('Become a Nation3 citizen')
11 | }),
12 |
13 | it('should load the /lock page', () => {
14 | cy.visit('/lock')
15 | cy.get('h2').contains('Lock $NATION to get $veNATION')
16 | }),
17 |
18 | it('should load the /liquidity page', () => {
19 | cy.visit('/liquidity')
20 | cy.get('h2').contains('$NATION liquidity rewards')
21 | })
22 | })
23 |
24 | export {}
25 |
--------------------------------------------------------------------------------
/ui/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/public/flag.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/lib/use-preferred-network.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { networkToId } from './network-id'
3 | import { useNetwork } from './use-wagmi'
4 |
5 | const preferredNetwork = process.env.NEXT_PUBLIC_CHAIN
6 |
7 | export default function usePreferredNetwork() {
8 | const { chain: activeChain } = useNetwork()
9 |
10 | const [isPreferredNetwork, setIsPreferredNetwork] = useState(false)
11 |
12 | useEffect(() => {
13 | if (activeChain?.id && activeChain?.id == networkToId(preferredNetwork)) {
14 | setIsPreferredNetwork(true)
15 | } else {
16 | setIsPreferredNetwork(false)
17 | }
18 | }, [activeChain?.id])
19 |
20 | return { isPreferredNetwork, preferredNetwork }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/frame.svg:
--------------------------------------------------------------------------------
1 |
2 | FrameLogo4
3 |
4 |
--------------------------------------------------------------------------------
/ui/lib/approve.ts:
--------------------------------------------------------------------------------
1 | import ERC20 from '../abis/ERC20.json'
2 | import { useContractRead } from './use-wagmi'
3 | import { useContractWrite } from './use-wagmi'
4 |
5 | export function useTokenAllowance({ token, address, spender }: any) {
6 | return useContractRead(
7 | {
8 | address: token,
9 | abi: ERC20.abi,
10 | watch: true,
11 | enabled: Boolean(token && address && spender),
12 | },
13 | 'allowance',
14 | [address, spender]
15 | )
16 | }
17 |
18 | export function useTokenApproval({ amountNeeded, token, spender }: any) {
19 | return useContractWrite(
20 | {
21 | address: token,
22 | abi: ERC20.abi,
23 | },
24 | 'approve',
25 | [spender, amountNeeded]
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 |
3 | const fourYearsInSeconds = 31556926 * 4
4 | const maxLockPeriod = ethers.BigNumber.from(fourYearsInSeconds);
5 |
6 | // threshold <= (lock amount) * (lock end - expiration) / (max lock period)
7 | // threshold * (max lock period) / (lock amount) <= (lock end - expiration)
8 | // expiration <= (lock end) - threshold * (max lock period) / (lock amount)
9 | export function getPassportExpirationDate(lockAmount: ethers.BigNumber, lockEnd: ethers.BigNumber, threshold: ethers.BigNumber): Date | undefined {
10 | if (lockAmount.isZero()) return undefined;
11 | const expiration = lockEnd.sub(threshold.mul(maxLockPeriod).div(lockAmount));
12 | return new Date(expiration.mul(1000).toNumber());
13 | }
14 |
--------------------------------------------------------------------------------
/ui/components/ErrorCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useErrorContext } from './ErrorProvider'
3 |
4 | export default function ErrorCard({ error }: any) {
5 | const { removeError } = useErrorContext()
6 |
7 | return (
8 |
9 |
10 |
Oopsie
11 |
12 | {error?.message ||
13 | error?.reason ||
14 | error?.data?.message ||
15 | 'Unknown error'}
16 |
17 |
18 |
removeError(error.key)}
21 | >
22 | ✕
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/contracts/deployments/sepolia.json:
--------------------------------------------------------------------------------
1 | {
2 | "nationToken": "0x23Ca3002706b71a440860E3cf8ff64679A00C9d7",
3 | "veNationToken": "0x8100e77899C24b0F7B516153F84868f850C034BF",
4 | "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
5 | "balancerPoolId": "0x1fe2f25dff7272e969fcb8b0fb978a9a6a18471b00020000000000000000007a",
6 | "balancerLPToken": "0x1FE2F25Dff7272E969fcb8b0FB978a9A6a18471b",
7 | "lpRewardsContract": "0x18C82f66F8caC20f6b066fd99e67f302AACcbc40",
8 | "nationPassportNFT": "0x11f30642277A70Dab74C6fAF4170a8b340BE2f98",
9 | "nationPassportNFTIssuer": "0xdad32e13E73ce4155a181cA0D350Fee0f2596940",
10 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL",
11 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link"
12 | }
13 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration-hook.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { useMemo } from "react";
3 | import { getPassportExpirationDate } from "./passport-expiration";
4 | import { usePassportRevokeUnderBalance } from "./passport-nft";
5 | import { useAccount } from "./use-wagmi";
6 | import { useVeNationLock } from "./ve-token";
7 | export function usePassportExpirationDate(): Date | undefined {
8 | const { address } = useAccount()
9 | const { data: veNationLock } = useVeNationLock(address)
10 |
11 | const { data: threshold } = usePassportRevokeUnderBalance()
12 |
13 | return useMemo(() => {
14 | if (!veNationLock) {
15 | return undefined;
16 | }
17 |
18 | const [lockAmount, lockEnd]: [ethers.BigNumber, ethers.BigNumber] = veNationLock;
19 | return getPassportExpirationDate(lockAmount, lockEnd, threshold);
20 | }, [veNationLock, threshold]);
21 | }
22 |
--------------------------------------------------------------------------------
/ui/components/HomeCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import GradientLink from '../components/GradientLink'
3 |
4 | export default function HomeCard({
5 | href,
6 | icon,
7 | title,
8 | children,
9 | linkText,
10 | }: any) {
11 | return (
12 |
13 |
14 |
15 | {icon}
16 |
17 | {title}
18 |
19 |
20 | {children}
21 |
22 |
23 | {linkText} →
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/ui/components/Head.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import React from 'react'
3 |
4 | export default function WebsiteHead({ title }: any) {
5 | const description = 'Citizen app for the citizens of Nation3'
6 | const image = 'https://app.nation3.org/social.jpg'
7 |
8 | return (
9 |
10 | Nation3 | {title}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/contracts/deployments/mainnet.json:
--------------------------------------------------------------------------------
1 | {
2 | "nationToken": "0x333A4823466879eeF910A04D473505da62142069",
3 | "veNationToken": "0xF7deF1D2FBDA6B74beE7452fdf7894Da9201065d",
4 | "nationDropContracts": [
5 | "0xcab2B7614351649870e4DCC3490Ab692bf3beD60",
6 | "0x0B8107EaCeC5e81Fe6E8594dB95E03CF50685f84"
7 | ],
8 | "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
9 | "balancerPoolId": "0x0bf37157d30dfe6f56757dcadff01aed83b08cd600020000000000000000019a",
10 | "balancerLPToken": "0x0BF37157d30dFe6f56757DCadff01AEd83b08cD6",
11 | "lpRewardsContract": "0xB23F8CC0e77332fE42b426D4AB3c9A893B9798a9",
12 | "nationPassportNFT": "0x3337dac9F251d4E403D6030E18e3cfB6a2cb1333",
13 | "nationPassportNFTIssuer": "0x279c0b6bfCBBA977eaF4ad1B2FFe3C208aa068aC",
14 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL",
15 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link"
16 | }
17 |
--------------------------------------------------------------------------------
/.github/workflows/ui_mainnet.yml:
--------------------------------------------------------------------------------
1 | name: /ui (Mainnet) CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | defaults:
15 | run:
16 | working-directory: ui
17 |
18 | strategy:
19 | matrix:
20 | node-version: [18.x, 20.x]
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | cache: 'npm'
30 | cache-dependency-path: ui/yarn.lock
31 | - run: npm install --global yarn
32 | - run: echo $(yarn --version)
33 | - run: cp .env.mainnet .env.local
34 | - run: yarn install
35 | - run: yarn build
36 | - run: yarn lint
37 | - run: yarn e2e:headless
38 | - run: yarn test
39 | - run: yarn test:coverage
40 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ## Related GitHub Issue
5 |
6 |
7 | closes #<👉INSERT_ISSUE_NUMBER_HERE👈>
8 |
9 | ## Screenshots (if appropriate):
10 |
11 |
12 |
13 | _Before_ code change:
14 | > ![]()
15 |
16 | _After_ code change:
17 | > ![]()
18 |
19 | ## How Has This Change Been Tested?
20 |
21 |
22 |
23 |
24 |
25 | - [ ] All [status checks](https://github.com/nation3/citizen-app/blob/main/.github/workflows/ui_mainnet.yml#L35) pass (build, lint, e2e, test)
26 | - [ ] Works on Sepolia preview deployment
27 | - [ ] Works on Mainnet preview deployment
28 |
29 | ## Are Any Admin Tasks Required?
30 |
31 |
32 | - [x] No admin tasks
33 |
--------------------------------------------------------------------------------
/ui/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 | import React from 'react'
3 |
4 | class WebsiteDocument extends Document {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 | {/* */}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 | }
36 |
37 | export default WebsiteDocument
38 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/coinbase.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jest/globals';
2 | import { BigNumber } from 'ethers';
3 | import { getPassportExpirationDate } from './passport-expiration';
4 |
5 | test("returns undefined if no lock amount", () => {
6 | const date = getPassportExpirationDate(BigNumber.from(0), BigNumber.from(0), BigNumber.from(String(1.5 * 10 ** 18)));
7 | expect(date).toBe(undefined);
8 | })
9 |
10 | const inFourYears = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 4;
11 |
12 | test("returns past date if below threshold", () => {
13 | const date = getPassportExpirationDate(BigNumber.from(1).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18)));
14 | expect(date).not.toBe(undefined);
15 | expect(date!.getTime() < Date.now()).toBe(true);
16 | })
17 |
18 | test("returns future date if over threshold", () => {
19 | const date = getPassportExpirationDate(BigNumber.from(2).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18)));
20 | expect(date).not.toBe(undefined);
21 | expect(date!.getTime() > Date.now()).toBe(true);
22 | })
23 |
--------------------------------------------------------------------------------
/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './pages/**/*.{js,ts,jsx,tsx}',
4 | './components/**/*.{js,ts,jsx,tsx}',
5 | ],
6 | theme: {
7 | fontFamily: {
8 | display: ['Poppins', 'sans-serif'],
9 | body: ['UniversalSans', 'sans-serif'],
10 | },
11 | fontWeight: {
12 | light: 200,
13 | normal: 300,
14 | medium: 400,
15 | semibold: 500,
16 | bold: 500,
17 | },
18 | extend: {
19 | colors: {
20 | n3blue: '#69C9FF',
21 | n3green: '#88F1BB',
22 | 'n3blue-100': '#DCFFFF',
23 | 'n3green-100': '#D5FFFF',
24 | n3bg: '#F4FAFF',
25 | n3nav: '#7395B2',
26 | },
27 | },
28 | },
29 | plugins: [require('daisyui')],
30 | daisyui: {
31 | themes: [
32 | {
33 | mytheme: {
34 | primary: '#69C9FF',
35 | secondary: '#88F1BB',
36 | accent: '#88F1BB',
37 | neutral: '#3d4451',
38 | 'primary-content': '#ffffff',
39 | 'base-100': '#ffffff',
40 | 'base-content': '#224059',
41 | },
42 | },
43 | ],
44 | },
45 | darkMode: 'media',
46 | }
47 |
--------------------------------------------------------------------------------
/ui/components/TimeRange.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function TimeRange({
4 | time,
5 | min,
6 | max,
7 | displaySteps,
8 | onChange,
9 | }: any) {
10 | const step = (min * 100) / max
11 |
12 | return (
13 | <>
14 | {
20 | onChange(new Date(parseFloat(e.target.value)))
21 | }}
22 | className="range range-secondary mt-4"
23 | step={step || 0}
24 | />
25 |
26 |
27 | {displaySteps ? (
28 | <>
29 | -
30 |
31 | 1 y
32 |
33 | 2 y
34 |
35 | 3 y
36 | >
37 | ) : (
38 | <>
39 | {' '}
40 | {new Date(min).toISOString().substring(0, 10)}
41 |
42 |
43 |
44 | >
45 | )}
46 |
47 | 4 y
48 |
49 | >
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nation3 Citizen App
2 |
3 | [](https://vercel.com/nation3dao/citizen-app/deployments)
4 |
5 |
6 |
7 | ---
8 |
9 | [](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 | > [](https://app.nation3.org)
14 |
15 |
16 | ## File Structure
17 |
18 | The code in this repository is structured into two main parts:
19 |
20 | ```
21 | .
22 | ├── contracts # The smart contracts
23 | └── ui # The user interface (UI) for interacting with the smart contracts
24 | ```
25 |
26 | ## Run the UI locally
27 |
28 | See [ui/README.md](ui/README.md)
29 |
--------------------------------------------------------------------------------
/ui/components/MainCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function MainCard({
4 | children,
5 | title,
6 | loading,
7 | gradientBg,
8 | maxWidthClassNames,
9 | }: any) {
10 | return (
11 | <>
12 | {!loading ? (
13 |
18 |
23 |
24 |
29 | {title}
30 |
31 | {children}
32 |
33 |
34 |
35 | ) : (
36 |
37 | )}
38 | >
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/ui_sepolia.yml:
--------------------------------------------------------------------------------
1 | name: /ui (Sepolia) CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | defaults:
15 | run:
16 | working-directory: ui
17 |
18 | strategy:
19 | matrix:
20 | node-version: [18.x, 20.x]
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | cache: 'npm'
30 | cache-dependency-path: ui/yarn.lock
31 | - run: npm install --global yarn
32 | - run: echo $(yarn --version)
33 | - run: cp .env.sepolia .env.local
34 | - run: yarn install
35 | - run: yarn build
36 | - run: yarn lint
37 | - run: yarn e2e:headless
38 | - run: yarn test
39 | - run: yarn test:coverage
40 | - name: Upload coverage reports to Codecov
41 | uses: codecov/codecov-action@v4.0.1
42 | with:
43 | token: ${{ secrets.CODECOV_TOKEN }}
44 | slug: nation3/citizen-app
45 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | # Nation3 Citizen App UI
2 |
3 | https://app.nation3.org
4 |
5 | ## Run the UI locally
6 |
7 | Navigate to the folder of the UI app:
8 | ```
9 | cd ui/
10 | ```
11 |
12 | Install the dependencies:
13 | ```
14 | yarn install
15 | ```
16 |
17 | Add variables to your local development environment:
18 | ```
19 | cp .env.mainnet .env.local
20 | ```
21 | or
22 | ```
23 | cp .env.sepolia .env.local
24 | ```
25 |
26 |
27 | Build:
28 | ```
29 | yarn build
30 | ```
31 |
32 | Lint:
33 | ```
34 | yarn lint
35 | ```
36 |
37 | Start the development server:
38 | ```
39 | yarn dev
40 | ```
41 |
42 | Then open http://localhost:3000 in a browser.
43 |
44 | ## Integration Testing
45 |
46 | Run the integration tests:
47 | ```
48 | yarn cypress
49 | ```
50 |
51 | Run the integration tests headlessly:
52 | ```
53 | yarn cypress:headless
54 | ```
55 |
56 | ## Unit Testing
57 |
58 | Run unit tests:
59 | ```
60 | yarn test
61 | ```
62 |
63 | ## Code Coverage
64 |
65 | [](https://codecov.io/gh/nation3/citizen-app)
66 |
67 | [](https://codecov.io/gh/nation3/citizen-app)
68 |
69 | Run code coverage:
70 | ```
71 | yarn test:coverage
72 | ```
73 |
--------------------------------------------------------------------------------
/ui/lib/sign-agreement.ts:
--------------------------------------------------------------------------------
1 | import {
2 | nationPassportNFTIssuer,
3 | nationPassportAgreementStatement,
4 | nationPassportAgreementURI
5 | } from './config'
6 | import { networkToId } from './network-id'
7 | import { useSignTypedData } from './use-wagmi'
8 |
9 | export const domain = {
10 | name: 'PassportIssuer',
11 | version: '1',
12 | chainId: networkToId(process.env.NEXT_PUBLIC_CHAIN),
13 | verifyingContract: nationPassportNFTIssuer,
14 | }
15 |
16 | export const types = {
17 | Agreement: [
18 | { name: 'statement', type: 'string' },
19 | { name: 'termsURI', type: 'string' },
20 | ],
21 | }
22 |
23 | export const value = {
24 | statement: `${nationPassportAgreementStatement}`,
25 | termsURI: `${nationPassportAgreementURI}`,
26 | }
27 |
28 | export function useSignAgreement({ onSuccess }: { onSuccess: Function }) {
29 | return useSignTypedData({
30 | domain,
31 | types,
32 | value,
33 | onSuccess,
34 | })
35 | }
36 |
37 | export async function storeSignature(signature: string, tx: string) {
38 | const response = await fetch('/api/store-signature', {
39 | method: 'POST',
40 | headers: {
41 | 'Content-Type': 'application/json',
42 | },
43 | body: JSON.stringify({ signature, tx }),
44 | })
45 | return response.json()
46 | }
47 |
--------------------------------------------------------------------------------
/ui/public/passport/ballot.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/components/SwitchNetworkBanner.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
2 | import React from 'react'
3 | import { useNetwork } from '../lib/use-wagmi'
4 |
5 | const chainIds = {
6 | mainnet: 1,
7 | sepolia: 11155111
8 | }
9 |
10 | type Indexable = {
11 | [key: string]: any
12 | }
13 |
14 | export default function SwitchNetworkBanner({ newNetwork }: any) {
15 | const { switchNetwork } = useNetwork()
16 |
17 | const capitalized = (network: any) =>
18 | network.charAt(0).toUpperCase() + network.slice(1)
19 |
20 | return (
21 |
22 |
23 |
24 | This dapp uses {capitalized(newNetwork)} as its preferred network.
25 |
26 |
27 | {/* There's a small chance the wallet used won't support switchNetwork, in which case the user needs to manually switch */}
28 | {/* Also check if we have specified a chain ID for the network */}
29 | {switchNetwork && (chainIds as Indexable)[newNetwork] && (
30 |
switchNetwork((chainIds as Indexable)[newNetwork])}
33 | >
34 | Switch to {capitalized(newNetwork)}
35 |
36 | )}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/ui/lib/numbers.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { BigNumber } from 'ethers'
3 | import { NumberType, transformNumber, isFixedDecimalsNumber } from './numbers'
4 |
5 | describe('transformNumber', () => {
6 | test("transformNumber to Number", () => {
7 | const actual = transformNumber(333, NumberType.number)
8 | expect(actual).toBe(Number(333))
9 | })
10 |
11 | test("transformNumber to BigNumber", () => {
12 | const actual = transformNumber(333, NumberType.bignumber)
13 | expect(actual).toBeInstanceOf(BigNumber)
14 | })
15 |
16 | test("transformNumber to String", () => {
17 | const actual = transformNumber(333, NumberType.string)
18 | expect(actual).toBe("333.000000000000000000")
19 | })
20 |
21 | test("transformNumber to String (2 decimals)", () => {
22 | const actual = transformNumber(333, NumberType.string, 2)
23 | expect(actual).toBe("333.00")
24 | })
25 | })
26 |
27 | describe('isFixedDecimalsNumber', () => {
28 | test('Is working for integers', () => {
29 | const isValid = isFixedDecimalsNumber(12345);
30 | expect(isValid).toBe(true)
31 | })
32 |
33 | test('Fails for too long decimals', () => {
34 | const isValid = isFixedDecimalsNumber('1.111111111111111111111111111111');
35 | expect(isValid).toBe(false)
36 | })
37 |
38 | test('Check "decimals" param is working properly', ()=> {
39 | const isValid = isFixedDecimalsNumber(1.12345, 2);
40 | expect(isValid).toBe(false)
41 | })
42 | })
--------------------------------------------------------------------------------
/ui/pages/api/store-signature.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 | import { NFTStorage, Blob } from 'nft.storage'
4 | import { nationPassportNFTIssuer } from '../../lib/config'
5 | import { provider } from '../../lib/connectors'
6 | import { domain, types, value } from '../../lib/sign-agreement'
7 |
8 | const client = new NFTStorage({
9 | token: process.env.NFTSTORAGE_KEY || '',
10 | })
11 |
12 | export default async function storeSignature(
13 | req: NextApiRequest,
14 | res: NextApiResponse
15 | ) {
16 | try {
17 | const prov = provider()
18 |
19 | const sender = ethers.utils.verifyTypedData(
20 | domain,
21 | types,
22 | value,
23 | req.body.signature
24 | )
25 | const { from, to, confirmations } = await prov.getTransaction(req.body.tx)
26 | // To prevent spam attacks: Check that the sender of the transaction is the same as the uploader,
27 | // the receiver is the passport issuer and the transaction is not very old
28 | if (
29 | sender === from &&
30 | to === nationPassportNFTIssuer &&
31 | confirmations < 14 * 60
32 | ) {
33 | const data = new Blob([
34 | JSON.stringify({ v: 1, sig: req.body.signature, tx: req.body.tx }),
35 | ])
36 | const cid = await client.storeBlob(data)
37 | res.status(200).json({ cid })
38 | }
39 | } catch (e) {
40 | console.error(e)
41 | res.status(500).json({
42 | error: `Storing your signature failed. This is your signature: ${req.body.signature}`,
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "citizen-app",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint",
9 | "cypress": "cypress open",
10 | "cypress:headless": "cypress run",
11 | "e2e": "start-server-and-test dev http://localhost:3000 cypress",
12 | "e2e:headless": "start-server-and-test dev http://localhost:3000 cypress:headless",
13 | "test": "jest",
14 | "test:coverage": "jest --coverage --collectCoverageFrom=./lib/**"
15 | },
16 | "dependencies": {
17 | "@heroicons/react": "^2.0.11",
18 | "@metamask/eth-sig-util": "^7.0.1",
19 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
20 | "@types/jest": "^29.5.12",
21 | "@types/node": "^20.12.11",
22 | "@types/react": "18.0.1",
23 | "@types/react-dom": "^18.0.4",
24 | "autoprefixer": "^10.4.0",
25 | "cypress": "^13.13.3",
26 | "daisyui": "^2.13.5",
27 | "eslint": "8.57.0",
28 | "eslint-config-next": "13",
29 | "eslint-config-prettier": "^9.1.0",
30 | "ethers": "^5.7.1",
31 | "jest": "^29.7.0",
32 | "next": "14",
33 | "nft.storage": "^7.1.1",
34 | "postcss": "^8.4.38",
35 | "prettier": "^3.3.3",
36 | "react": "^18.3.1",
37 | "react-animated-3d-card": "^1.0.2",
38 | "react-blockies": "^1.4.1",
39 | "react-confetti": "^6.0.1",
40 | "react-dom": "^18.3.1",
41 | "react-use": "^17.5.0",
42 | "start-server-and-test": "^2.0.5",
43 | "tailwindcss": "^3.4.10",
44 | "ts-jest": "^28.0.8",
45 | "typescript": "^4.7.4",
46 | "use-nft": "^0.12.0",
47 | "wagmi": "0.12.8"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ui/lib/use-wagmi.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useConnect as _useConnect,
3 | useAccount as _useAccount,
4 | useBalance as _useBalance,
5 | useNetwork as _useNetwork,
6 | useContractRead as _useContractRead,
7 | useContractWrite as _useContractWrite,
8 | useSignTypedData as _useSignTypedData,
9 | } from 'wagmi'
10 | import { useStaticCall as _useStaticCall } from './static-call'
11 | import { useHandleError } from './use-handle-error'
12 |
13 | export function useConnect() {
14 | return useHandleError(_useConnect())
15 | }
16 |
17 | // custom extension of wagmi
18 | export function useStaticCall(params: any) {
19 | return useHandleError(_useStaticCall(params))
20 | }
21 |
22 | export function useAccount(params?: any) {
23 | return useHandleError(_useAccount(params))
24 | }
25 |
26 | export function useNetwork() {
27 | return useHandleError(_useNetwork())
28 | }
29 |
30 | export function useBalance(params: any) {
31 | return useHandleError(_useBalance(params))
32 | }
33 |
34 | export function useContractRead(
35 | config: any,
36 | functionName: any,
37 | args?: any,
38 | overrides?: any,
39 | throwOnRevert?: any
40 | ) {
41 | return useHandleError(
42 | _useContractRead({ ...config, functionName, args, overrides }),
43 | throwOnRevert
44 | )
45 | }
46 |
47 | export function useContractWrite(
48 | config: any,
49 | functionName: any,
50 | args: any,
51 | overrides?: any,
52 | throwOnRevert?: any
53 | ) {
54 | return useHandleError(
55 | _useContractWrite({ ...config, functionName, args, overrides }),
56 | throwOnRevert
57 | )
58 | }
59 |
60 | export function useSignTypedData(params: any) {
61 | return useHandleError(_useSignTypedData(params))
62 | }
63 |
--------------------------------------------------------------------------------
/ui/public/fonts/stylesheet.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Bossa';
3 | src: url('Bossa-Black.woff2') format('woff2'),
4 | url('Bossa-Black.woff') format('woff');
5 | font-weight: 900;
6 | font-style: normal;
7 | font-display: swap;
8 | }
9 |
10 | @font-face {
11 | font-family: 'Bossa';
12 | src: url('Bossa-Regular.woff2') format('woff2'),
13 | url('Bossa-Regular.woff') format('woff');
14 | font-weight: normal;
15 | font-style: normal;
16 | font-display: swap;
17 | }
18 |
19 | @font-face {
20 | font-family: 'Bossa';
21 | src: url('Bossa-Light.woff2') format('woff2'),
22 | url('Bossa-Light.woff') format('woff');
23 | font-weight: 300;
24 | font-style: normal;
25 | font-display: swap;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Bossa';
30 | src: url('Bossa-Medium.woff2') format('woff2'),
31 | url('Bossa-Medium.woff') format('woff');
32 | font-weight: 500;
33 | font-style: normal;
34 | font-display: swap;
35 | }
36 |
37 | @font-face {
38 | font-family: 'Bossa';
39 | src: url('Bossa-Bold.woff2') format('woff2'),
40 | url('Bossa-Bold.woff') format('woff');
41 | font-weight: bold;
42 | font-style: normal;
43 | font-display: swap;
44 | }
45 |
46 | @font-face {
47 | font-family: 'UniversalSans';
48 | src: url('UniversalSans-Italic.woff2') format('woff2'),
49 | url('UniversalSans-Italic.woff') format('woff');
50 | font-weight: normal;
51 | font-style: italic;
52 | font-display: swap;
53 | }
54 |
55 | @font-face {
56 | font-family: 'UniversalSans';
57 | src: url('UniversalSans-Regular.woff2') format('woff2'),
58 | url('UniversalSans-Regular.woff') format('woff');
59 | font-weight: normal;
60 | font-style: normal;
61 | font-display: swap;
62 | }
63 |
--------------------------------------------------------------------------------
/ui/lib/numbers.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber, ethers } from 'ethers'
2 |
3 | function stringToNumber(string: any, decimals: any) {
4 | return Number(string).toFixed(decimals)
5 | }
6 |
7 | export enum NumberType {
8 | number = 'number',
9 | bignumber = 'bignumber',
10 | string = 'string',
11 | }
12 |
13 | export function transformNumber(
14 | num: number | BigNumber | string,
15 | to: NumberType,
16 | decimals = 18
17 | ): BigNumber | string | number {
18 | if (!num) {
19 | return to === NumberType.bignumber ? ethers.BigNumber.from('0') : 0
20 | }
21 |
22 | if (to === NumberType.bignumber) {
23 | if (num instanceof ethers.BigNumber) return num
24 |
25 | return ethers.utils.parseUnits(
26 | typeof num === 'string' ? num : num.toString(),
27 | decimals
28 | )
29 | } else if (to === NumberType.number) {
30 | if (typeof num === 'number') return num
31 |
32 | if (num instanceof ethers.BigNumber) {
33 | return stringToNumber(ethers.utils.formatUnits(num, 18), decimals)
34 | } else if (typeof num === 'string') {
35 | return parseFloat(num).toFixed(decimals)
36 | }
37 | } else if (to === NumberType.string) {
38 | if (typeof num === 'string') return num
39 |
40 | if (num instanceof ethers.BigNumber) {
41 | return stringToNumber(
42 | ethers.utils.formatUnits(num, 18),
43 | decimals
44 | ).toString()
45 | } else if (typeof num === 'number') {
46 | return num.toFixed(decimals).toString()
47 | }
48 | }
49 | return 0
50 | }
51 |
52 | export function isFixedDecimalsNumber(value: any, decimals = 18) {
53 | const NUMBER_REGEX = RegExp(`^(\\d*\\.{0,1}\\d{0,${decimals}}$)`)
54 | const isValid = value.toString().match(NUMBER_REGEX)
55 |
56 | return Boolean(isValid);
57 | }
58 |
--------------------------------------------------------------------------------
/ui/cypress/e2e/liquidity.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Liquidity', () => {
2 | beforeEach(() => {
3 | cy.visit('/liquidity')
4 |
5 | cy.intercept("GET", "https://api.coingecko.com/api/v3/simple/price?ids=nation3,ethereum&vs_currencies=usd", {
6 | body: {
7 | "ethereum": {
8 | "usd": 2672.05
9 | },
10 | "nation3": {
11 | "usd": 30.75
12 | }
13 | },
14 | });
15 | });
16 |
17 | it('stake: type a deposit amount', () => {
18 |
19 | // Expect default value to be '0'
20 | cy.get('#depositValue')
21 | .invoke('val')
22 | .then(val => {
23 | expect(val).to.equal('0')
24 | })
25 |
26 | // Clear the default '0' value before typing a number
27 | cy.get('#depositValue').clear()
28 |
29 | // Type a number
30 | cy.get('#depositValue').type('3.3333')
31 |
32 | // Expect the value to no longer be '0'
33 | cy.get('#depositValue')
34 | .invoke('val')
35 | .then(val => {
36 | expect(val).to.equal('3.3333')
37 | })
38 | })
39 |
40 | it('unstake: type a withdrawal amount', () => {
41 |
42 | // Click the "Unstake" tab
43 | cy.get("#unstakeTab").click()
44 |
45 | // Expect default value to be '0'
46 | cy.get('#withdrawalValue')
47 | .invoke('val')
48 | .then(val => {
49 | expect(val).to.equal('0')
50 | })
51 |
52 | // Clear the default '0' value before typing a number
53 | cy.get('#withdrawalValue').clear()
54 |
55 | // Type a number
56 | cy.get('#withdrawalValue').type('0.3333')
57 |
58 | // Expect the value to no longer be '0'
59 | cy.get('#withdrawalValue')
60 | .invoke('val')
61 | .then(val => {
62 | expect(val).to.equal('0.3333')
63 | })
64 | })
65 | })
66 |
67 | export {}
68 |
--------------------------------------------------------------------------------
/ui/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { useEffect, useState } from 'react'
3 | import React from 'react'
4 | import { NftProvider } from 'use-nft'
5 | import { WagmiConfig, createClient } from 'wagmi'
6 | import { connectors, provider as externalProvider } from '../lib/connectors'
7 | import { ErrorProvider } from '../components/ErrorProvider'
8 | import Layout from '../components/Layout'
9 | import '../styles/globals.css'
10 |
11 | function App({ Component, pageProps }: any) {
12 | const [client, setClient] = useState()
13 |
14 | useEffect(() => {
15 | let provider = externalProvider
16 |
17 | const userProvider =
18 | window.ethereum || (window as unknown as any).web3?.currentProvider
19 | if (userProvider && process.env.NEXT_PUBLIC_CHAIN !== 'local') {
20 | provider = () => {
21 | console.log(
22 | `Provider: Connected to the user's provider on chain with ID ${parseInt(
23 | userProvider.networkVersion,
24 | )}`,
25 | )
26 | return new ethers.providers.Web3Provider(
27 | userProvider,
28 | process.env.NEXT_PUBLIC_CHAIN,
29 | )
30 | }
31 | }
32 | setClient(
33 | createClient({
34 | autoConnect: true,
35 | connectors,
36 | provider: provider(),
37 | }),
38 | )
39 | }, [])
40 |
41 | return (
42 | <>
43 | {client && (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | )}
54 | >
55 | )
56 | }
57 |
58 | export default App
59 |
--------------------------------------------------------------------------------
/ui/public/passport/contactless.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/lib/static-call.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react'
2 | import { useContract, useSigner } from 'wagmi'
3 |
4 | // Hook for calling a contract write method without executing, getting the result without changing state (and thus gas fees)
5 | export function useStaticCall({
6 | address,
7 | abi,
8 | methodName,
9 | args,
10 | defaultData,
11 | skip = false,
12 |
13 | // set to false if you'd like a reverted txn to be ignored and use the default data
14 | throwOnRevert = true,
15 |
16 | genericErrorMessage = 'Error calling contract',
17 | }: any) {
18 | args = args?.length === 0 ? undefined : args // args should be undefined if there are no arguments, make sure here
19 |
20 | const [loading, setLoading] = useState(true)
21 | const [error, setError] = useState(null)
22 | const [data, setData] = useState(defaultData)
23 |
24 | const { data: signer } = useSigner()
25 |
26 | const contract = useContract({
27 | address,
28 | abi,
29 | signerOrProvider: signer,
30 | })!
31 |
32 | const stringifiedArgs = useMemo(() => JSON.stringify(args), [args]);
33 | useEffect(() => {
34 | const call = async () => {
35 | if (skip || !signer) {
36 | return
37 | }
38 | try {
39 | const result = args
40 | ? await contract.callStatic[methodName](...args)
41 | : await contract.callStatic[methodName]()
42 | setData(result)
43 | } catch (error) {
44 | if (throwOnRevert) {
45 | console.error(error)
46 | setError({ ...(error as any), message: genericErrorMessage })
47 | }
48 | } finally {
49 | setLoading(false)
50 | }
51 | }
52 |
53 | call()
54 | // eslint-disable-next-line react-hooks/exhaustive-deps
55 | }, [
56 | skip,
57 | address,
58 | abi,
59 | methodName,
60 | genericErrorMessage,
61 | signer,
62 | stringifiedArgs,
63 | ])
64 |
65 | return {
66 | data,
67 | error,
68 | loading,
69 | method: contract.callStatic[methodName],
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/ui/lib/connectors.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { configureChains, } from 'wagmi'
3 | import { mainnet, sepolia } from 'wagmi/chains'
4 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'
5 | import { InjectedConnector } from 'wagmi/connectors/injected'
6 | import { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect'
7 | import CoinbaseWalletIcon from '../public/icons/connectors/coinbase.svg'
8 | import FrameIcon from '../public/icons/connectors/frame.svg'
9 | import MetaMaskIcon from '../public/icons/connectors/metamask.svg'
10 | import WalletConnectIcon from '../public/icons/connectors/walletconnect.svg'
11 | import { networkToId } from './network-id'
12 |
13 | const chains = [mainnet, sepolia]
14 |
15 | export function provider() {
16 | if (process.env.NEXT_PUBLIC_CHAIN === 'local') {
17 | console.log('Provider: Connected to localhost provider')
18 | return new ethers.providers.JsonRpcProvider(
19 | 'http://127.0.0.1:7545',
20 | networkToId(process.env.NEXT_PUBLIC_CHAIN)
21 | )
22 | } else {
23 | console.log(
24 | `Provider: Connected to the external provider on chain ${process.env.NEXT_PUBLIC_CHAIN}`
25 | )
26 | return ethers.getDefaultProvider(process.env.NEXT_PUBLIC_CHAIN, {
27 | infura: process.env.NEXT_PUBLIC_INFURA_ID,
28 | alchemy: process.env.NEXT_PUBLIC_ALCHEMY_ID,
29 | quorum: 1,
30 | });
31 | }
32 | }
33 |
34 | export const connectors = [
35 | new InjectedConnector({
36 | chains,
37 | options: { shimDisconnect: true },
38 | }),
39 | new WalletConnectConnector({
40 | chains,
41 | options: {
42 | showQrModal: true,
43 | projectId: 'de21254f0716238419606243642a9266',
44 | },
45 | }),
46 | new CoinbaseWalletConnector({
47 | chains,
48 | options: {
49 | appName: 'Nation3 app',
50 | },
51 | }),
52 | ]
53 |
54 | export const connectorIcons = {
55 | Frame: FrameIcon,
56 | MetaMask: MetaMaskIcon,
57 | WalletConnect: WalletConnectIcon,
58 | 'Coinbase Wallet': CoinbaseWalletIcon,
59 | }
--------------------------------------------------------------------------------
/ui/lib/balancer.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import BalancerVault from '../abis/BalancerVault.json'
3 | import { balancerVault } from './config'
4 | import { NumberType, transformNumber } from './numbers'
5 | import { useContractRead } from './use-wagmi'
6 |
7 | export function useBalancerPool(id: any) {
8 | const { data: poolData, isLoading } = useContractRead(
9 | {
10 | address: balancerVault,
11 | abi: BalancerVault.abi,
12 | },
13 | 'getPoolTokens',
14 | [id],
15 | undefined,
16 | process.env.NEXT_PUBLIC_CHAIN === 'mainnet'
17 | )
18 |
19 | const [poolValue, setPoolValue] = useState(0)
20 | const [nationPrice, setNationPrice] = useState(0)
21 | const [ethPrice, setEthPrice] = useState(0)
22 |
23 | useEffect(() => {
24 | async function fetchData() {
25 | const priceRes = await fetch(
26 | 'https://api.coingecko.com/api/v3/simple/price?ids=nation3,ethereum&vs_currencies=usd'
27 | )
28 | const { nation3, ethereum } = await priceRes.json()
29 | setNationPrice(nation3.usd)
30 | setEthPrice(ethereum.usd)
31 | }
32 | fetchData()
33 | }, [])
34 |
35 | useEffect(() => {
36 | if (!isLoading && poolData && nationPrice && ethPrice) {
37 | let nationBalance
38 | let wethBalance
39 | if (process.env.NEXT_PUBLIC_CHAIN === 'mainnet') {
40 | const balances = poolData[1]
41 | nationBalance = balances[0]
42 | wethBalance = balances[1]
43 | } else {
44 | nationBalance = transformNumber(333, NumberType.bignumber)
45 | wethBalance = transformNumber(333, NumberType.bignumber)
46 | }
47 |
48 | if (nationBalance && wethBalance) {
49 | const nationValue = nationBalance.mul(Math.round(nationPrice))
50 | const ethValue = wethBalance.mul(Math.round(ethPrice))
51 | const totalValue = nationValue.add(ethValue)
52 | setPoolValue(totalValue)
53 | }
54 | }
55 | }, [isLoading, poolData, ethPrice, nationPrice])
56 | return { poolValue, nationPrice, isLoading }
57 | }
58 |
--------------------------------------------------------------------------------
/ui/public/passport/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/components/ErrorProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useCallback, useContext, useState } from 'react'
2 | import { useNetwork } from 'wagmi'
3 | import { networkToId } from '../lib/network-id'
4 |
5 | const ErrorContext = createContext({} as any)
6 |
7 | function ErrorProvider({ children }: any) {
8 | const [errors, setErrors] = useState([] as any[])
9 | let [count, setCount] = useState(0)
10 | const { chain: activeChain } = useNetwork()
11 |
12 | const addError = useCallback(
13 | (newErrors: any) => {
14 | if (
15 | newErrors &&
16 | newErrors[0] &&
17 | activeChain?.id &&
18 | activeChain.id == networkToId(process.env.NEXT_PUBLIC_CHAIN)
19 | ) {
20 | for (let error of newErrors) {
21 | console.error(error)
22 | if (error instanceof Error) {
23 | error = JSON.parse(
24 | JSON.stringify(error, Object.getOwnPropertyNames(error)),
25 | )
26 | // Don't add the error if it's "invalid address or ENS name",
27 | // no idea why those errors appear in the first place.
28 | if (
29 | error.code &&
30 | (error.code === 'INVALID_ARGUMENT' ||
31 | error.code === 'MISSING_ARGUMENT')
32 | ) {
33 | return
34 | }
35 | }
36 | setErrors((prev) => [...prev, { key: prev.length, ...error }])
37 | setCount((prev) => prev + 1)
38 | }
39 | }
40 | },
41 | [activeChain?.id],
42 | )
43 |
44 | const removeError = (key: any) => {
45 | if (key > -1) {
46 | setErrors(errors.filter((error: any) => error.key !== key))
47 | setCount(--count)
48 | }
49 | }
50 |
51 | const context = { errors, addError, removeError }
52 | return (
53 | {children}
54 | )
55 | }
56 |
57 | function useErrorContext() {
58 | const errors = useContext(ErrorContext)
59 | if (errors === undefined) {
60 | throw new Error('useErrorContext must be used within a ErrorProvider')
61 | }
62 | return errors
63 | }
64 |
65 | export { ErrorProvider, useErrorContext }
66 |
--------------------------------------------------------------------------------
/ui/lib/passport-nft.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useNft } from 'use-nft'
3 | import PassportIssuer from '../abis/PassportIssuer.json'
4 | import { nationPassportNFT, nationPassportNFTIssuer } from './config'
5 | import { useContractRead, useContractWrite } from './use-wagmi'
6 |
7 | const nftIssuerContractParams = {
8 | address: nationPassportNFTIssuer,
9 | abi: PassportIssuer.abi,
10 | }
11 |
12 | export function useHasPassport(address: any) {
13 | const [hasPassport, setHasPassport] = useState(false)
14 | const { data: passportStatus, isLoading } = usePassportStatus(address)
15 |
16 | useEffect(() => {
17 | if (passportStatus == 1) {
18 | setHasPassport(true)
19 | }
20 | }, [passportStatus, isLoading])
21 |
22 | return { hasPassport, isLoading }
23 | }
24 |
25 | export function usePassportStatus(address: any) {
26 | return useContractRead(
27 | {
28 | ...nftIssuerContractParams,
29 | watch: true,
30 | enabled: Boolean(address),
31 | },
32 | 'passportStatus',
33 | [address],
34 | undefined,
35 | false
36 | )
37 | }
38 |
39 | export function useClaimRequiredBalance() {
40 | return useContractRead(
41 | {
42 | ...nftIssuerContractParams,
43 | watch: true,
44 | },
45 | 'claimRequiredBalance',
46 | undefined,
47 | )
48 | }
49 |
50 | export function useClaimPassport() {
51 | return useContractWrite(
52 | {
53 | address: nationPassportNFTIssuer,
54 | abi: PassportIssuer.abi,
55 | },
56 | 'claim',
57 | undefined
58 | )
59 | }
60 |
61 | export function usePassport(address: any) {
62 | const { data: id, isLoading: loadingID } = useContractRead(
63 | {
64 | ...nftIssuerContractParams,
65 | enable: Boolean(address)
66 | },
67 | 'passportId',
68 | [address],
69 | undefined,
70 | false
71 | )
72 | console.log(`Passport ID ${id}`)
73 | const { loading, nft } = useNft(nationPassportNFT || '', id)
74 | return { data: { id, nft }, isLoading: loadingID || loading }
75 | }
76 |
77 | export function usePassportRevokeUnderBalance() {
78 | return useContractRead(
79 | {
80 | ...nftIssuerContractParams,
81 | },
82 | 'revokeUnderBalance',
83 | undefined
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/ui/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/ui/components/ActionButton.tsx:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { ReactNode } from 'react'
3 | import { useWaitForTransaction } from 'wagmi'
4 | import usePreferredNetwork from '../lib/use-preferred-network'
5 | import { useAccount, useNetwork } from '../lib/use-wagmi'
6 | import ActionNeedsTokenApproval from './ActionNeedsTokenApproval'
7 |
8 | export interface ActionButtonProps {
9 | className?: string
10 | children: ReactNode
11 | action: any
12 | preAction?: Function
13 | postAction?: Function
14 | approval?: {
15 | token: string
16 | spender: string
17 | amountNeeded: BigNumber | number | string
18 | approveText: string
19 | allowUnlimited?: boolean
20 | }
21 | }
22 |
23 | export default function ActionButton({
24 | className,
25 | children,
26 | action,
27 | preAction,
28 | postAction,
29 | approval,
30 | }: ActionButtonProps) {
31 | const { address } = useAccount()
32 | const { writeAsync, data, isLoadingOverride } = action
33 | const { isLoading } = useWaitForTransaction({
34 | hash: data?.hash,
35 | })
36 | const onClick = async () => {
37 | preAction && preAction()
38 | const tx = await writeAsync()
39 | tx?.wait && (await tx.wait())
40 | postAction && postAction()
41 | }
42 |
43 | const { chain: activeChain } = useNetwork()
44 | const { isPreferredNetwork } = usePreferredNetwork()
45 |
46 | return (
47 | <>
48 | {!isPreferredNetwork ? (
49 |
50 | {!activeChain?.id ? 'Not connected' : 'Wrong network'}
51 |
52 | ) : !address ? (
53 |
54 | {children}
55 |
56 | ) : isLoading || isLoadingOverride ? (
57 |
58 |
59 |
60 | ) : approval ? (
61 |
67 | {children}
68 |
69 | ) : (
70 |
71 | {children}
72 |
73 | )}
74 | >
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/ui/lib/config.ts:
--------------------------------------------------------------------------------
1 | const zeroAddress = '0x0000000000000000000000000000000000000000'
2 |
3 | interface DeploymentConfig {
4 | nationToken: string,
5 | veNationToken: string,
6 | balancerVault: string,
7 | balancerPoolId: string,
8 | balancerLPToken: string,
9 | lpRewardsContract: string,
10 | nationPassportNFT: string,
11 | nationPassportNFTIssuer: string,
12 | nationPassportAgreementStatement: string,
13 | nationPassportAgreementURI: string,
14 | }
15 |
16 | interface Config {
17 | nationToken: string,
18 | veNationToken: string,
19 | balancerDomain: string,
20 | balancerVault: string,
21 | balancerPoolId: string,
22 | balancerLPToken: string,
23 | etherscanDomain: string,
24 | lpRewardsContract: string,
25 | mobilePassportDomain: string,
26 | nationPassportNFT: string,
27 | nationPassportNFTIssuer: string,
28 | nationPassportAgreementStatement: string,
29 | nationPassportAgreementURI: string,
30 | }
31 |
32 |
33 | const chain = process.env.NEXT_PUBLIC_CHAIN || 'sepolia';
34 | const defaultConfig = require(`../../contracts/deployments/${chain}.json`) as DeploymentConfig
35 | const config: Config = {
36 | nationToken: defaultConfig.nationToken || zeroAddress,
37 | veNationToken: defaultConfig.veNationToken || zeroAddress,
38 | balancerDomain: chain === 'mainnet' ? 'https://app.balancer.fi/#/ethereum' : `https://app.balancer.fi/#/${chain}`,
39 | balancerVault: defaultConfig.balancerVault || zeroAddress,
40 | balancerPoolId: defaultConfig.balancerPoolId || zeroAddress,
41 | balancerLPToken: defaultConfig.balancerLPToken || zeroAddress,
42 | etherscanDomain: chain === 'mainnet' ? 'https://etherscan.io' : `https://${chain}.etherscan.io`,
43 | lpRewardsContract: defaultConfig.lpRewardsContract || zeroAddress,
44 | mobilePassportDomain: chain === 'mainnet' ? 'https://passports.nation3.org' : `https://mobile-passport-${chain}.vercel.app`,
45 | nationPassportNFT: defaultConfig.nationPassportNFT || zeroAddress,
46 | nationPassportNFTIssuer: defaultConfig.nationPassportNFTIssuer || zeroAddress,
47 | nationPassportAgreementStatement: defaultConfig.nationPassportAgreementStatement || "",
48 | nationPassportAgreementURI: defaultConfig.nationPassportAgreementURI || "",
49 | }
50 |
51 | console.log(config)
52 |
53 | export const {
54 | nationToken,
55 | veNationToken,
56 | balancerDomain,
57 | balancerVault,
58 | balancerPoolId,
59 | balancerLPToken,
60 | etherscanDomain,
61 | lpRewardsContract,
62 | mobilePassportDomain,
63 | nationPassportNFT,
64 | nationPassportNFTIssuer,
65 | nationPassportAgreementStatement,
66 | nationPassportAgreementURI,
67 | } = config
68 |
--------------------------------------------------------------------------------
/ui/lib/ve-token.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import VotingEscrow from '../abis/VotingEscrow.json'
3 | import { veNationToken } from '../lib/config'
4 | import { useBalance, useContractRead, useContractWrite } from './use-wagmi'
5 |
6 | const contractParams = {
7 | address: veNationToken,
8 | abi: VotingEscrow.abi,
9 | }
10 |
11 | export function useVeNationBalance(address: any) {
12 | return useBalance({
13 | address,
14 | token: veNationToken,
15 | watch: true,
16 | enabled: !!address,
17 | })
18 | }
19 |
20 | let gasLimits = {
21 | locked: 330000,
22 | create_lock: 600000,
23 | increase_amount: 600000,
24 | increase_unlock_time: 600000,
25 | withdraw: 480000,
26 | }
27 |
28 | export function useVeNationLock(address: any) {
29 | return useContractRead(
30 | {
31 | ...contractParams,
32 | watch: true,
33 | enabled: !!address,
34 | },
35 | 'locked',
36 | [address]
37 | )
38 | }
39 |
40 | export function useVeNationCreateLock(amount: any, time: any) {
41 | return useContractWrite(
42 | contractParams,
43 | 'create_lock',
44 | [amount, time],
45 | { gasLimit: gasLimits.create_lock },
46 | )
47 | }
48 |
49 | export function useVeNationIncreaseLock({
50 | newAmount,
51 | currentTime,
52 | newTime,
53 | }: any) {
54 | const { writeAsync: increaseLockAmount, data: lockAmountData } =
55 | useVeNationIncreaseLockAmount(newAmount)
56 | const { writeAsync: increaseLockTime, data: lockTimeData } =
57 | useVeNationIncreaseLockTime(newTime)
58 | return useMemo(() => {
59 | if (newAmount && newAmount.gt(0)) {
60 | return { writeAsync: increaseLockAmount, data: lockAmountData }
61 | }
62 | if (newTime && currentTime && newTime.gt(currentTime)) {
63 | return { writeAsync: increaseLockTime, data: lockTimeData }
64 | }
65 | return {}
66 | }, [newAmount, currentTime, newTime, increaseLockAmount, increaseLockTime, lockAmountData, lockTimeData])
67 | }
68 |
69 | export function useVeNationIncreaseLockAmount(amount: any) {
70 | return useContractWrite(
71 | contractParams,
72 | 'increase_amount',
73 | [amount],
74 | { gasLimit: gasLimits.increase_amount }
75 | )
76 | }
77 |
78 | export function useVeNationIncreaseLockTime(time: any) {
79 | return useContractWrite(
80 | contractParams,
81 | 'increase_unlock_time',
82 | [time],
83 | { gasLimit: gasLimits.increase_unlock_time }
84 | )
85 | }
86 |
87 | export function useVeNationWithdrawLock() {
88 | return useContractWrite(
89 | contractParams,
90 | 'withdraw',
91 | [],
92 | { gasLimit: gasLimits.withdraw }
93 | )
94 | }
95 |
96 | export function useVeNationSupply() {
97 | return useContractRead(contractParams, 'totalSupply')
98 | }
99 |
--------------------------------------------------------------------------------
/ui/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | UserPlusIcon,
3 | LockClosedIcon,
4 | PlusIcon,
5 | } from '@heroicons/react/24/outline'
6 | import Image from 'next/image'
7 | import Link from 'next/link'
8 | import React from 'react'
9 | import { balancerDomain, nationToken } from '../lib/config'
10 | import GradientLink from '../components/GradientLink'
11 | import Head from '../components/Head'
12 | import HomeCard from '../components/HomeCard'
13 | import flag from '../public/flag.svg'
14 |
15 | export default function Index() {
16 | return (
17 | <>
18 |
19 |
20 |
21 | Welcome to Nation3
22 |
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 |
60 |
64 |
65 |
66 | Approve unlimited
67 |
68 | setApproveUnlimited(e.target.checked)}
73 | />
74 |
75 |
76 | )}
77 |
78 | {approveText}
79 |
80 |
81 | )
82 | ) : (
83 |
84 |
85 |
86 | )}
87 | >
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/ui/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/public/passport/logo-plain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/pages/citizen.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { useState } from 'react'
3 | // @ts-ignore
4 | import Card from 'react-animated-3d-card'
5 | import { useSignMessage } from 'wagmi'
6 | import { mobilePassportDomain } from '../lib/config'
7 | import { usePassportExpirationDate } from '../lib/passport-expiration-hook'
8 | import { usePassport } from '../lib/passport-nft'
9 | import { useAccount } from '../lib/use-wagmi'
10 | import Confetti from '../components/Confetti'
11 | import Head from '../components/Head'
12 | import PassportExpiration from '../components/PassportExpiration'
13 | import BallotIcon from '../public/passport/ballot.svg'
14 | import DiscordIcon from '../public/passport/discord.svg'
15 | import AddToWallet from '../public/passport/wallet.svg'
16 |
17 | export default function Citizen() {
18 | const { address } = useAccount()
19 | const { data: passportData } = usePassport(address)
20 | const [confettiNumber, setConfettiNumber] = useState>([])
21 |
22 | const addConfetti = () => {
23 | setConfettiNumber([...confettiNumber, confettiNumber.length])
24 | }
25 |
26 | const { signMessage: signMessageAndDownloadPass } = useSignMessage({
27 | message: 'I am the holder of this Nation3 passport',
28 | onSuccess(data) {
29 | console.log('signMessageAndDownloadPass data:', data)
30 | const downloadPassURI: string = `${mobilePassportDomain}/api/downloadPass?address=${address}&signature=${data}&platform=Apple`
31 | console.log('downloadPassURI:', downloadPassURI)
32 | window.location.href = downloadPassURI
33 | },
34 | onError(error) {
35 | console.error('signMessageAndDownloadPass error:', error)
36 | },
37 | })
38 |
39 | const passportExpirationDate = usePassportExpirationDate()
40 |
41 | return (
42 | <>
43 | {confettiNumber.map((number: Number) => (
44 |
45 | ))}
46 |
47 | {passportData?.nft ? (
48 |
49 |
50 |
51 | Welcome, citizen
52 |
53 |
54 |
55 | 420 ? '390px' : '340px',
58 | height: window.innerWidth > 420 ? '450px' : '400px',
59 | cursor: 'pointer',
60 | position: 'relative',
61 | }}
62 | onClick={() => addConfetti()}
63 | >
64 |
65 |
66 |
67 |
68 |
99 |
100 |
101 |
102 | ) : (
103 |
104 |
105 |
106 | )}
107 | >
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/metamask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/ui/public/passport/art.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/ui/lib/liquidity-rewards.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { useState, useEffect } from 'react'
3 | import { lpRewardsContract, balancerLPToken } from '../lib/config'
4 | import LiquidityRewardsDistributor from '../abis/BoostedLiquidityDistributor.json'
5 | import ERC20 from '../abis/ERC20.json'
6 | import { NumberType, transformNumber } from './numbers'
7 | import {
8 | useBalance,
9 | useStaticCall,
10 | useContractRead,
11 | useContractWrite,
12 | } from './use-wagmi'
13 |
14 | const contractParams = {
15 | address: lpRewardsContract,
16 | abi: LiquidityRewardsDistributor.abi,
17 | }
18 |
19 | export function useLiquidityRewards({ nationPrice, poolValue, address }: any) {
20 | const { data: totalRewards, isLoading: totalRewardsLoading } =
21 | useContractRead(contractParams, 'totalRewards', undefined, undefined, false)
22 | const months = 6
23 |
24 | const { data: unclaimedRewards, loading: unclaimedRewardsLoading } =
25 | useStaticCall({
26 | ...contractParams,
27 | methodName: 'claimRewards',
28 | defaultData: transformNumber(0, NumberType.bignumber),
29 | throwOnRevert: false, // assumes a reverted transaction means no claimable rewards
30 | skip: !address,
31 | })
32 |
33 | const { data: userDeposit, isLoading: userDepositLoading } = useContractRead(
34 | {
35 | ...contractParams,
36 | watch: true,
37 | enabled: Boolean(address),
38 | },
39 | 'userDeposit',
40 | [address]
41 | )
42 |
43 | const { data: totalDeposit, isLoading: totalDepositLoading } =
44 | useContractRead(contractParams, 'totalDeposit', undefined, undefined, false)
45 |
46 | const { data: lpTokensSupply, isLoading: lpTokensSupplyLoading } =
47 | useContractRead(
48 | { address: balancerLPToken, abi: ERC20.abi },
49 | 'totalSupply',
50 | undefined,
51 | undefined,
52 | false
53 | )
54 |
55 | const { data: userBalance, isLoading: userBalanceLoading } = useContractRead(
56 | {
57 | ...contractParams,
58 | watch: true,
59 | enabled: Boolean(address),
60 | },
61 | 'userBalance',
62 | [address]
63 | )
64 |
65 | const [liquidityRewardsAPY, setLiquidityRewardsAPY] = useState(
66 | transformNumber(0, NumberType.bignumber) as BigNumber
67 | )
68 |
69 | useEffect(() => {
70 | if (totalRewards && poolValue && totalDeposit && lpTokensSupply) {
71 | if (totalDeposit.gt(transformNumber(0, NumberType.bignumber))){
72 | setLiquidityRewardsAPY(
73 | totalRewards
74 | .mul(transformNumber(12 / months, NumberType.bignumber))
75 | .mul(transformNumber(nationPrice, NumberType.bignumber, 2))
76 | .div(poolValue.mul(totalDeposit).div(lpTokensSupply))
77 | )
78 | }
79 | }
80 | }, [
81 | poolValue,
82 | totalDeposit,
83 | lpTokensSupply,
84 | nationPrice,
85 | totalRewards,
86 | totalRewardsLoading,
87 | totalDepositLoading,
88 | lpTokensSupplyLoading,
89 | ])
90 |
91 | return {
92 | liquidityRewardsAPY,
93 | unclaimedRewards,
94 | userDeposit,
95 | totalDeposit,
96 | userBalance,
97 | loading:
98 | totalRewardsLoading ||
99 | unclaimedRewardsLoading ||
100 | userDepositLoading ||
101 | totalDepositLoading ||
102 | userBalanceLoading,
103 | }
104 | }
105 |
106 | export function usePoolTokenBalance(address: any) {
107 | return useBalance({
108 | address: address,
109 | token: balancerLPToken,
110 | watch: true,
111 | enabled: address,
112 | })
113 | }
114 |
115 | // userDeposit = amount of LP tokens staked by user
116 | // totalDeposit = amount of LP tokens in rewards contract
117 | // userVotingPower = veNationBalance
118 | // totalVotingPower = veNATION supply
119 | export function useVeNationBoost({
120 | userDeposit,
121 | totalDeposit,
122 | userVeNation,
123 | totalVeNation,
124 | userBalance,
125 | }: any) {
126 | const [boost, setBoost] = useState({
127 | canBoost: false,
128 | })
129 | useEffect(() => {
130 | if (
131 | userDeposit &&
132 | totalDeposit &&
133 | userVeNation &&
134 | totalVeNation &&
135 | userBalance
136 | ) {
137 | const n = {
138 | userDeposit: parseFloat(
139 | transformNumber(userDeposit, NumberType.string) as string
140 | ),
141 | totalDeposit: parseFloat(
142 | transformNumber(totalDeposit, NumberType.string) as string
143 | ),
144 | userVeNation: parseFloat(
145 | transformNumber(userVeNation, NumberType.string) as string
146 | ),
147 | totalVeNation: parseFloat(
148 | transformNumber(totalVeNation, NumberType.string) as string
149 | ),
150 | userBalance: parseFloat(
151 | transformNumber(userBalance, NumberType.string) as string
152 | ),
153 | }
154 |
155 | const baseBalance = n.userDeposit * 0.4
156 |
157 | let boostedBalance =
158 | baseBalance +
159 | ((n.totalDeposit * n.userVeNation) / n.totalVeNation) * (60 / 100)
160 |
161 | boostedBalance = Math.min(boostedBalance, n.userDeposit)
162 |
163 | const potentialBoost = boostedBalance / baseBalance
164 |
165 | boostedBalance = Math.min(boostedBalance, n.userDeposit)
166 |
167 | const currentBoost = n.userBalance / baseBalance
168 |
169 | setBoost({
170 | currentBoost: transformNumber(
171 | Math.max(currentBoost, 1),
172 | NumberType.bignumber
173 | ),
174 | potentialBoost: transformNumber(potentialBoost, NumberType.bignumber),
175 | canBoost:
176 | Math.trunc(potentialBoost * 10) > Math.trunc(currentBoost * 10),
177 | })
178 | }
179 | }, [userDeposit, totalDeposit, userVeNation, totalVeNation, userBalance])
180 |
181 | return boost
182 | }
183 |
184 | export function useBoostedAPY({ defaultAPY, boostMultiplier }: any) {
185 | const [apy, setAPY] = useState(
186 | parseFloat(transformNumber(defaultAPY, NumberType.string) as string)
187 | )
188 | useEffect(() => {
189 | let defaultAPYasNumber = transformNumber(
190 | defaultAPY,
191 | NumberType.number
192 | ) as number
193 | let boostMultiplierAsNumber = transformNumber(
194 | boostMultiplier,
195 | NumberType.number
196 | ) as number
197 |
198 | if (defaultAPYasNumber != 0 && boostMultiplierAsNumber != 0) {
199 | setAPY(defaultAPYasNumber * boostMultiplierAsNumber)
200 | }
201 | }, [defaultAPY, boostMultiplier])
202 | return apy
203 | }
204 |
205 | // Using Wagmi's contractWrite directly, getting a "no signer connected" error otherwise
206 | export function useClaimRewards() {
207 | return useContractWrite(contractParams, 'claimRewards', undefined, { gasLimit: 300000 })
208 | }
209 |
210 | export function useDeposit(amount: any) {
211 | return useContractWrite(contractParams, 'deposit', [amount], { gasLimit: 300000 })
212 | }
213 |
214 | export function useWithdraw(amount: any) {
215 | return useContractWrite(contractParams, 'withdraw', [amount], { gasLimit: 300000 })
216 | }
217 |
218 | export function useWithdrawAndClaim() {
219 | return useContractWrite(contractParams, 'withdrawAndClaim', undefined, { gasLimit: 300000 })
220 | }
221 |
--------------------------------------------------------------------------------
/ui/abis/ERC20.json:
--------------------------------------------------------------------------------
1 | {
2 | "abi": [
3 | { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" },
4 | { "inputs": [], "name": "CallerIsNotAuthorized", "type": "error" },
5 | { "inputs": [], "name": "TargetIsZeroAddress", "type": "error" },
6 | {
7 | "anonymous": false,
8 | "inputs": [
9 | {
10 | "indexed": true,
11 | "internalType": "address",
12 | "name": "owner",
13 | "type": "address"
14 | },
15 | {
16 | "indexed": true,
17 | "internalType": "address",
18 | "name": "spender",
19 | "type": "address"
20 | },
21 | {
22 | "indexed": false,
23 | "internalType": "uint256",
24 | "name": "amount",
25 | "type": "uint256"
26 | }
27 | ],
28 | "name": "Approval",
29 | "type": "event"
30 | },
31 | {
32 | "anonymous": false,
33 | "inputs": [
34 | {
35 | "indexed": true,
36 | "internalType": "address",
37 | "name": "previousController",
38 | "type": "address"
39 | },
40 | {
41 | "indexed": true,
42 | "internalType": "address",
43 | "name": "newController",
44 | "type": "address"
45 | }
46 | ],
47 | "name": "ControlTransferred",
48 | "type": "event"
49 | },
50 | {
51 | "anonymous": false,
52 | "inputs": [
53 | {
54 | "indexed": true,
55 | "internalType": "address",
56 | "name": "previousOwner",
57 | "type": "address"
58 | },
59 | {
60 | "indexed": true,
61 | "internalType": "address",
62 | "name": "newOwner",
63 | "type": "address"
64 | }
65 | ],
66 | "name": "OwnershipTransferred",
67 | "type": "event"
68 | },
69 | {
70 | "anonymous": false,
71 | "inputs": [
72 | {
73 | "indexed": true,
74 | "internalType": "address",
75 | "name": "from",
76 | "type": "address"
77 | },
78 | {
79 | "indexed": true,
80 | "internalType": "address",
81 | "name": "to",
82 | "type": "address"
83 | },
84 | {
85 | "indexed": false,
86 | "internalType": "uint256",
87 | "name": "amount",
88 | "type": "uint256"
89 | }
90 | ],
91 | "name": "Transfer",
92 | "type": "event"
93 | },
94 | {
95 | "inputs": [],
96 | "name": "DOMAIN_SEPARATOR",
97 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
98 | "stateMutability": "view",
99 | "type": "function"
100 | },
101 | {
102 | "inputs": [],
103 | "name": "PERMIT_TYPEHASH",
104 | "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
105 | "stateMutability": "view",
106 | "type": "function"
107 | },
108 | {
109 | "inputs": [
110 | { "internalType": "address", "name": "", "type": "address" },
111 | { "internalType": "address", "name": "", "type": "address" }
112 | ],
113 | "name": "allowance",
114 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
115 | "stateMutability": "view",
116 | "type": "function"
117 | },
118 | {
119 | "inputs": [
120 | { "internalType": "address", "name": "spender", "type": "address" },
121 | { "internalType": "uint256", "name": "amount", "type": "uint256" }
122 | ],
123 | "name": "approve",
124 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
125 | "stateMutability": "nonpayable",
126 | "type": "function"
127 | },
128 | {
129 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }],
130 | "name": "balanceOf",
131 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
132 | "stateMutability": "view",
133 | "type": "function"
134 | },
135 | {
136 | "inputs": [],
137 | "name": "controller",
138 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
139 | "stateMutability": "view",
140 | "type": "function"
141 | },
142 | {
143 | "inputs": [],
144 | "name": "decimals",
145 | "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
146 | "stateMutability": "view",
147 | "type": "function"
148 | },
149 | {
150 | "inputs": [
151 | { "internalType": "address", "name": "to", "type": "address" },
152 | { "internalType": "uint256", "name": "amount", "type": "uint256" }
153 | ],
154 | "name": "mint",
155 | "outputs": [],
156 | "stateMutability": "nonpayable",
157 | "type": "function"
158 | },
159 | {
160 | "inputs": [],
161 | "name": "name",
162 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
163 | "stateMutability": "view",
164 | "type": "function"
165 | },
166 | {
167 | "inputs": [{ "internalType": "address", "name": "", "type": "address" }],
168 | "name": "nonces",
169 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
170 | "stateMutability": "view",
171 | "type": "function"
172 | },
173 | {
174 | "inputs": [],
175 | "name": "owner",
176 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
177 | "stateMutability": "view",
178 | "type": "function"
179 | },
180 | {
181 | "inputs": [
182 | { "internalType": "address", "name": "owner", "type": "address" },
183 | { "internalType": "address", "name": "spender", "type": "address" },
184 | { "internalType": "uint256", "name": "value", "type": "uint256" },
185 | { "internalType": "uint256", "name": "deadline", "type": "uint256" },
186 | { "internalType": "uint8", "name": "v", "type": "uint8" },
187 | { "internalType": "bytes32", "name": "r", "type": "bytes32" },
188 | { "internalType": "bytes32", "name": "s", "type": "bytes32" }
189 | ],
190 | "name": "permit",
191 | "outputs": [],
192 | "stateMutability": "nonpayable",
193 | "type": "function"
194 | },
195 | {
196 | "inputs": [],
197 | "name": "removeControl",
198 | "outputs": [],
199 | "stateMutability": "nonpayable",
200 | "type": "function"
201 | },
202 | {
203 | "inputs": [],
204 | "name": "renounceOwnership",
205 | "outputs": [],
206 | "stateMutability": "nonpayable",
207 | "type": "function"
208 | },
209 | {
210 | "inputs": [],
211 | "name": "symbol",
212 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
213 | "stateMutability": "view",
214 | "type": "function"
215 | },
216 | {
217 | "inputs": [],
218 | "name": "totalSupply",
219 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
220 | "stateMutability": "view",
221 | "type": "function"
222 | },
223 | {
224 | "inputs": [
225 | { "internalType": "address", "name": "to", "type": "address" },
226 | { "internalType": "uint256", "name": "amount", "type": "uint256" }
227 | ],
228 | "name": "transfer",
229 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
230 | "stateMutability": "nonpayable",
231 | "type": "function"
232 | },
233 | {
234 | "inputs": [
235 | {
236 | "internalType": "address",
237 | "name": "newController",
238 | "type": "address"
239 | }
240 | ],
241 | "name": "transferControl",
242 | "outputs": [],
243 | "stateMutability": "nonpayable",
244 | "type": "function"
245 | },
246 | {
247 | "inputs": [
248 | { "internalType": "address", "name": "from", "type": "address" },
249 | { "internalType": "address", "name": "to", "type": "address" },
250 | { "internalType": "uint256", "name": "amount", "type": "uint256" }
251 | ],
252 | "name": "transferFrom",
253 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
254 | "stateMutability": "nonpayable",
255 | "type": "function"
256 | },
257 | {
258 | "inputs": [
259 | { "internalType": "address", "name": "newOwner", "type": "address" }
260 | ],
261 | "name": "transferOwnership",
262 | "outputs": [],
263 | "stateMutability": "nonpayable",
264 | "type": "function"
265 | }
266 | ]
267 | }
268 |
--------------------------------------------------------------------------------
/ui/pages/join.tsx:
--------------------------------------------------------------------------------
1 | import { LockClosedIcon, SparklesIcon } from '@heroicons/react/24/outline'
2 | import { ethers } from 'ethers'
3 | import Link from 'next/link'
4 | import { useRouter } from 'next/router'
5 | import { useEffect, useMemo, useState } from 'react'
6 | import { useWaitForTransaction } from 'wagmi'
7 | import {
8 | nationToken,
9 | balancerDomain,
10 | nationPassportAgreementStatement,
11 | nationPassportAgreementURI,
12 | } from '../lib/config'
13 | import { useNationBalance } from '../lib/nation-token'
14 | import { NumberType, transformNumber } from '../lib/numbers'
15 | import {
16 | useClaimPassport,
17 | useHasPassport,
18 | useClaimRequiredBalance,
19 | } from '../lib/passport-nft'
20 | import { storeSignature, useSignAgreement } from '../lib/sign-agreement'
21 | import { useAccount } from '../lib/use-wagmi'
22 | import { useVeNationBalance } from '../lib/ve-token'
23 | import ActionButton from '../components/ActionButton'
24 | import Balance from '../components/Balance'
25 | import Confetti from '../components/Confetti'
26 | import GradientLink from '../components/GradientLink'
27 | import Head from '../components/Head'
28 | import MainCard from '../components/MainCard'
29 |
30 | export default function Join() {
31 | const { address } = useAccount()
32 | const { data: nationBalance, isLoading: nationBalanceLoading } =
33 | useNationBalance(address)
34 | const { data: veNationBalance, isLoading: veNationBalanceLoading } =
35 | useVeNationBalance(address)
36 | const { hasPassport, isLoading: hasPassportLoading } = useHasPassport(address)
37 | const { data: claimRequiredBalance, isLoading: claimRequiredBalanceLoading } = useClaimRequiredBalance()
38 | const requiredBalance = useMemo(() => {
39 | if (claimRequiredBalanceLoading) {
40 | return -1
41 | }
42 | return transformNumber(claimRequiredBalance, NumberType.string, 0) as number
43 | }, [claimRequiredBalance, claimRequiredBalanceLoading])
44 |
45 | const { writeAsync: claim, data: claimData } = useClaimPassport()
46 | const { isLoading: claimPassportLoading } = useWaitForTransaction({
47 | hash: claimData?.hash,
48 | })
49 | const { isLoading: signatureLoading, signTypedData } = useSignAgreement({
50 | onSuccess: async (signature: string) => {
51 | const sigs = ethers.utils.splitSignature(signature)
52 | const tx = await claim({
53 | recklesslySetUnpreparedArgs: [sigs.v, sigs.r, sigs.s],
54 | })
55 |
56 | // The signature will be stored permanently on the Ethereum blockchain,
57 | // so uploading it to IPFS is only a nice to have
58 | await storeSignature(signature, tx.hash)
59 | },
60 | })
61 |
62 | const signAndClaim = {
63 | isLoadingOverride: signatureLoading || claimPassportLoading,
64 | writeAsync: signTypedData,
65 | }
66 |
67 | const router = useRouter()
68 |
69 | const changeUrl = () => {
70 | router.replace('/join?mintingPassport=1', undefined, { shallow: true })
71 | }
72 |
73 | useEffect(() => {
74 | if (hasPassport) {
75 | setTimeout(() => {
76 | router.push('/citizen')
77 | }, 5000)
78 | }
79 | }, [hasPassport, hasPassportLoading, router])
80 |
81 | const [action, setAction] = useState({
82 | mint: transformNumber(0, NumberType.bignumber),
83 | lockAndMint: transformNumber(0, NumberType.bignumber),
84 | })
85 |
86 | useEffect(() => {
87 | if (!nationBalance || !veNationBalance) return
88 | setAction({
89 | mint: veNationBalance.value.gte(
90 | transformNumber(claimRequiredBalance as number, NumberType.bignumber),
91 | ),
92 | lockAndMint: nationBalance.value
93 | .mul(4)
94 | .gte(
95 | transformNumber(
96 | (claimRequiredBalance as number) / 4,
97 | NumberType.bignumber,
98 | ),
99 | ),
100 | })
101 | }, [
102 | nationBalance,
103 | nationBalanceLoading,
104 | veNationBalance,
105 | veNationBalanceLoading,
106 | claimRequiredBalance,
107 | ])
108 |
109 | return (
110 | <>
111 |
112 | {hasPassport && }
113 |
114 |
115 |
118 | Lock $NATION
119 |
120 |
125 | Claim passport
126 |
127 |
130 | Adore your passport
131 |
132 |
133 |
134 | {!hasPassport ? (
135 | <>
136 |
137 | To become a citizen, you need to mint a passport NFT by holding at
138 | least{' '}
139 |
140 | {requiredBalance == -1 ? '...' : requiredBalance} $veNATION
141 |
142 | . This is to make sure all citizens are economically aligned.
143 |
144 |
145 | Your $NATION won't be taken away from you. As your lock matures,
146 | you can either withdraw your tokens or increase the lock time to
147 | keep citizenship. Passport NFTs represent membership and are
148 | currently not transferable.
149 |
150 |
151 | {nationPassportAgreementStatement}:{' '}
152 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | Needed balance
165 |
166 |
167 | {requiredBalance == -1 ? '...' : requiredBalance}
168 |
169 |
$veNATION
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | Your balance
178 |
179 |
180 |
185 |
186 |
$veNATION
187 |
188 |
189 |
190 | {action.mint ? (
191 |
196 | Claim
197 |
198 | ) : action.lockAndMint ? (
199 | <>
200 |
201 |
202 | Lock $NATION
203 |
204 |
205 | >
206 | ) : (
207 |
213 | Buy $NATION
214 |
215 | )}
216 | >
217 | ) : (
218 | <>
219 |
220 | We are delighted to welcome you to Nation3 as a fellow citizen.
221 | You will be taken to your passport in a few seconds ✨
222 |
223 |
224 |
225 |
226 | >
227 | )}
228 |
229 | >
230 | )
231 | }
232 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Enable incremental compilation */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
13 | // "jsx": "preserve", /* Specify what JSX code is generated. */
14 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
15 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
16 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
17 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
18 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
19 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
20 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
21 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
22 | /* Modules */
23 | "module": "commonjs" /* Specify what module code is generated. */,
24 | // "rootDir": "./", /* Specify the root folder within your source files. */
25 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
26 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
27 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
28 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
29 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
30 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
31 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
32 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
33 | /* JavaScript Support */
34 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
35 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
36 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
37 | /* Emit */
38 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
39 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
40 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
41 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
42 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
43 | // "outDir": "./", /* Specify an output folder for all emitted files. */
44 | // "removeComments": true, /* Disable emitting comments. */
45 | // "noEmit": true, /* Disable emitting files from a compilation. */
46 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
47 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
48 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
49 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
50 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
52 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
53 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
54 | // "newLine": "crlf", /* Set the newline character for emitting files. */
55 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
56 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
57 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
58 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
59 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
60 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
61 | /* Interop Constraints */
62 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
63 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
64 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
65 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */,
66 | "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
67 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
68 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
69 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
70 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
71 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
72 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
73 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
74 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
75 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
76 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
77 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
78 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
79 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
80 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
81 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
82 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
83 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
84 | /* Completeness */
85 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
86 | "skipLibCheck": true /* Skip type checking all .d.ts files. */,
87 | "lib": ["dom", "dom.iterable", "esnext"],
88 | "allowJs": true,
89 | "noEmit": true,
90 | "incremental": true,
91 | "moduleResolution": "node",
92 | "resolveJsonModule": true,
93 | "isolatedModules": true,
94 | "jsx": "preserve"
95 | },
96 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
97 | "exclude": ["node_modules"]
98 | }
99 |
--------------------------------------------------------------------------------
/ui/public/passport/art2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/walletconnect.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/pages/liquidity.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowTrendingUpIcon,
3 | CurrencyDollarIcon,
4 | SparklesIcon,
5 | CalculatorIcon,
6 | InformationCircleIcon,
7 | } from '@heroicons/react/24/outline'
8 | import { useEffect, useState } from 'react'
9 | import React from 'react'
10 | import { useBalancerPool } from '../lib/balancer'
11 | import {
12 | balancerPoolId,
13 | balancerLPToken,
14 | lpRewardsContract,
15 | // veNationRewardsMultiplier,
16 | balancerDomain,
17 | } from '../lib/config'
18 | import {
19 | useLiquidityRewards,
20 | usePoolTokenBalance,
21 | useVeNationBoost,
22 | useBoostedAPY,
23 | useDeposit,
24 | useWithdraw,
25 | useWithdrawAndClaim,
26 | useClaimRewards,
27 | } from '../lib/liquidity-rewards'
28 | import { NumberType, transformNumber } from '../lib/numbers'
29 | import { useAccount } from '../lib/use-wagmi'
30 | import { useVeNationBalance, useVeNationSupply } from '../lib/ve-token'
31 | import ActionButton from '../components/ActionButton'
32 | import Balance from '../components/Balance'
33 | import EthersInput from '../components/EthersInput'
34 | import GradientLink from '../components/GradientLink'
35 | import Head from '../components/Head'
36 | import MainCard from '../components/MainCard'
37 |
38 | export default function Liquidity() {
39 | const { address } = useAccount()
40 |
41 | const { data: veNationBalance, isLoading: veNationBalanceLoading } =
42 | useVeNationBalance(address)
43 | const {
44 | poolValue,
45 | nationPrice,
46 | isLoading: poolLoading,
47 | } = useBalancerPool(balancerPoolId)
48 |
49 | const { data: poolTokenBalance, isLoading: poolTokenBalanceLoading } =
50 | usePoolTokenBalance(address)
51 |
52 | const {
53 | liquidityRewardsAPY,
54 | unclaimedRewards,
55 | userDeposit,
56 | totalDeposit,
57 | userBalance,
58 | loading: liquidityRewardsLoading,
59 | } = useLiquidityRewards({
60 | nationPrice,
61 | poolValue,
62 | address: address,
63 | })
64 |
65 | const { data: veNationSupply } = useVeNationSupply()
66 |
67 | const { currentBoost, potentialBoost, canBoost } = useVeNationBoost({
68 | userDeposit,
69 | totalDeposit,
70 | userVeNation: veNationBalance?.value,
71 | totalVeNation: veNationSupply,
72 | userBalance,
73 | })
74 |
75 | const boostedAPY = useBoostedAPY({
76 | defaultAPY: liquidityRewardsAPY,
77 | boostMultiplier: currentBoost,
78 | })
79 |
80 | const [depositValue, setDepositValue] = useState(0)
81 | const [withdrawalValue, setWithdrawalValue] = useState('0')
82 | const deposit = useDeposit(
83 | transformNumber(depositValue, NumberType.bignumber)
84 | )
85 | const withdraw = useWithdraw(
86 | transformNumber(withdrawalValue, NumberType.bignumber)
87 | )
88 |
89 | // @ts-expect-error
90 | const claimRewards = useClaimRewards(unclaimedRewards)
91 | const withdrawAndClaimRewards = useWithdrawAndClaim()
92 | const [activeTab, setActiveTab] = useState(0)
93 |
94 | return (
95 | <>
96 |
97 |
98 |
99 |
100 | Provide liquidity in the pool and then deposit the pool token here.{' '}
101 |
106 |
107 | {/* Get up to {veNationRewardsMultiplier}x more rewards with $veNATION.{' '}
108 | */}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | Total liquidity
119 |
120 |
121 |
122 |
132 |
133 |
134 |
135 |
136 |
139 |
140 |
Rewards APY
141 |
142 |
143 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
Your veNATION
160 |
161 |
162 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | Your boosted APY
177 |
178 |
179 |
180 |
185 |
186 |
187 |
188 | {canBoost && (
189 |
190 |
191 |
192 |
193 |
194 | You can boost your APY to{' '}
195 |
196 | {transformNumber(
197 | (
198 | ((transformNumber(
199 | liquidityRewardsAPY ?? 0,
200 | NumberType.number,
201 | ) as number) /
202 | 10 ** 18) *
203 | potentialBoost
204 | ).toString(),
205 | NumberType.number,
206 | 2,
207 | ) + '%'}
208 |
209 | . To do so, claim your current rewards.
210 |
211 |
212 |
213 | )}
214 |
215 |
216 |
217 |
218 |
222 | Claim
223 |
224 |
225 |
226 |
Your rewards
227 |
228 |
229 |
230 |
231 |
232 |
NATION tokens
233 |
234 |
235 |
236 |
237 |
238 |
255 |
256 |
257 | {activeTab === 0 ? (
258 | <>
259 |
260 | Available to deposit:{' '}
261 | {' '}
265 | LP tokens
266 |
267 |
268 |
269 |
277 |
278 |
281 | poolTokenBalance &&
282 | setDepositValue(poolTokenBalance?.formatted)
283 | }
284 | >
285 | Max
286 |
287 |
288 |
289 |
290 |
300 | Deposit
301 |
302 |
303 | >
304 | ) : (
305 | <>
306 |
307 | Available to withdraw:{' '}
308 | LP tokens
309 |
310 |
311 |
312 | {
319 | setWithdrawalValue(value)
320 | }}
321 | />
322 |
323 |
326 | userDeposit &&
327 | setWithdrawalValue(
328 | transformNumber(
329 | userDeposit,
330 | NumberType.string,
331 | ) as string,
332 | )
333 | }
334 | >
335 | Max
336 |
337 |
338 |
339 |
340 |
344 | Withdraw
345 |
346 |
347 |
351 | userDeposit &&
352 | setWithdrawalValue(
353 | transformNumber(
354 | userDeposit,
355 | NumberType.string,
356 | ) as string,
357 | )
358 | }
359 | >
360 | Withdraw all and claim
361 |
365 |
366 |
367 |
368 |
369 | >
370 | )}
371 |
372 |
373 |
374 |
375 | >
376 | )
377 | }
--------------------------------------------------------------------------------
/ui/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowRightOnRectangleIcon,
3 | ArrowTopRightOnSquareIcon,
4 | BanknotesIcon,
5 | Bars3Icon,
6 | ChevronDownIcon,
7 | ChevronRightIcon,
8 | CurrencyDollarIcon,
9 | HomeIcon,
10 | KeyIcon,
11 | LockClosedIcon,
12 | NewspaperIcon,
13 | PlusIcon,
14 | Squares2X2Icon,
15 | UserIcon,
16 | UserPlusIcon,
17 | UsersIcon,
18 | XCircleIcon,
19 | } from '@heroicons/react/24/outline'
20 | import Image from 'next/image'
21 | import Link from 'next/link'
22 | import { useRouter } from 'next/router'
23 | import { useState } from 'react'
24 | // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message
25 | import Blockies from 'react-blockies'
26 | import { useConnect, useDisconnect, useEnsName } from 'wagmi'
27 | import { balancerDomain, etherscanDomain, nationToken } from '../lib/config'
28 | import { connectorIcons } from '../lib/connectors'
29 | import { useAccount } from '../lib/use-wagmi'
30 | import Logo from '../public/logo.svg'
31 | import ErrorCard from './ErrorCard'
32 | import { useErrorContext } from './ErrorProvider'
33 | import PassportCheck from './PassportCheck'
34 | import PreferredNetworkWrapper from './PreferredNetworkWrapper'
35 |
36 | type Indexable = {
37 | [key: string]: any
38 | }
39 |
40 | const navigation = [
41 | {
42 | name: 'Start',
43 | href: '/',
44 | icon: ,
45 | },
46 | {
47 | name: 'Become a citizen',
48 | href: '/join',
49 | icon: ,
50 | },
51 | {
52 | name: 'Citizen directory',
53 | href: 'https://citizens.nation3.org',
54 | icon: ,
55 | },
56 | {
57 | name: 'Lock tokens',
58 | href: '/lock',
59 | icon: ,
60 | },
61 | {
62 | name: 'Liquidity rewards',
63 | href: '/liquidity',
64 | icon: ,
65 | },
66 | {
67 | name: 'Claim basic income',
68 | href: 'https://income.nation3.org',
69 | icon: ,
70 | },
71 | {
72 | name: 'Buy $NATION',
73 | href: `${balancerDomain}/swap/ether/${nationToken}`,
74 | icon: ,
75 | },
76 | {
77 | name: 'Homepage',
78 | href: 'https://nation3.org',
79 | icon: ,
80 | },
81 | {
82 | name: 'Wiki',
83 | href: 'https://wiki.nation3.org',
84 | icon: ,
85 | },
86 | ]
87 |
88 | export default function Layout({ children }: any) {
89 | const router = useRouter()
90 | const {
91 | connectors,
92 | connect,
93 | error: connectError,
94 | data: connectData,
95 | } = useConnect()
96 | const { address } = useAccount()
97 |
98 | const { data: ensName } = useEnsName({ address: address ?? '' })
99 | const { disconnect } = useDisconnect()
100 | const [nav, setNav] = useState(navigation)
101 | const errorContext = useErrorContext()
102 |
103 | const onPassportChecked = (hasPassport: boolean) => {
104 | if (hasPassport) {
105 | navigation[1].name = 'Welcome, citizen'
106 | navigation[1].href = '/citizen'
107 | setNav(navigation)
108 | if (router.pathname === '/join' && !router.query.mintingPassport) {
109 | router.push('/citizen')
110 | }
111 | } else {
112 | if (router.pathname === '/citizen') {
113 | router.push('/join')
114 | }
115 | }
116 | }
117 |
118 | const layout = (
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
154 |
155 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | {/* Logo placeholder for Dark Mode */}
167 | {/*
168 |
169 | */}
170 |
171 |
172 |
173 |
174 | {nav.map((item: any) => (
175 |
178 | // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
179 | (document.getElementById('side-drawer').checked = false)
180 | }
181 | key={item.href}
182 | >
183 | {item.href.charAt(0) === '/' ? (
184 |
188 | {item.icon}
189 | {item.name}
190 |
191 |
192 | ) : (
193 |
201 | {item.icon}
202 | {item.name}
203 |
204 |
205 | )}
206 |
207 | ))}
208 |
209 |
210 | {address ? (
211 |
212 |
213 |
214 |
215 |
216 | {ensName ? (
217 | ensName
218 | ) : (
219 | {`${(
220 | (address as string) ?? ''
221 | ).substring(0, 6)}...${address.slice(-4)}`}
222 | )}
223 |
224 |
225 |
226 | ) : (
227 |
228 |
232 | Sign in
233 |
234 |
235 | )}
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
249 |
253 | ✕
254 |
255 |
256 | {address ? (
257 | <>
258 | Account
259 |
260 | Connected to {connectData?.connector?.name}
261 |
262 |
294 | >
295 | ) : (
296 | <>
297 |
298 | Sign in by connecting your account
299 |
300 |
301 | You can choose from these providers:
302 | {connectError ? (
303 |
304 |
305 |
306 | {connectError?.message || 'Failed to connect'}
307 |
308 |
309 | ) : (
310 | ''
311 | )}
312 |
313 |
314 | {connectors.map((connector) => (
315 |
316 | connect({ connector })}
319 | className="dark:bg-slate-700
320 | dark:hover:bg-slate-600 mb-0.5"
321 | >
322 | {(connectorIcons as Indexable)[connector.name] ? (
323 |
324 |
328 |
329 | ) : (
330 |
331 | )}
332 | {connector.name}
333 | {!connector.ready && ' (unsupported)'}
334 |
335 |
336 | ))}
337 |
338 |
339 |
340 | New to Ethereum?{' '}
341 |
347 | Learn more about wallets
348 |
349 | .
350 |
351 | By using this software, you agree to{' '}
352 |
358 | its terms of use
359 |
360 | .
361 |
362 | >
363 | )}
364 |
365 |
366 | {errorContext?.errors ? (
367 |
368 |
369 | {errorContext.errors.map((error: any) => (
370 |
371 | ))}
372 |
373 |
374 | ) : (
375 | ''
376 | )}
377 |
378 | )
379 |
380 | if (address) {
381 | return (
382 |
383 | {layout}
384 |
385 | )
386 | } else {
387 | return layout
388 | }
389 | }
390 |
--------------------------------------------------------------------------------