├── contracts
└── deployments
│ ├── .gitkeep
│ ├── sepolia.json
│ ├── goerli.json
│ └── mainnet.json
├── .github
├── CODEOWNERS
├── codecov.yml
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── ui_mainnet.yml
│ └── ui_sepolia.yml
├── ui
├── next.config.js
├── public
│ ├── favicon.ico
│ ├── social.jpg
│ ├── passport
│ │ ├── art3.png
│ │ ├── art4.png
│ │ ├── icon-plain.svg
│ │ ├── ballot.svg
│ │ ├── contactless.svg
│ │ ├── discord.svg
│ │ ├── logo-plain.svg
│ │ ├── art.svg
│ │ └── art2.svg
│ ├── fonts
│ │ ├── Bossa-Bold.woff
│ │ ├── Bossa-Black.woff
│ │ ├── Bossa-Black.woff2
│ │ ├── Bossa-Bold.woff2
│ │ ├── Bossa-Light.woff
│ │ ├── Bossa-Light.woff2
│ │ ├── Bossa-Medium.woff
│ │ ├── Bossa-Medium.woff2
│ │ ├── Bossa-Regular.woff
│ │ ├── Bossa-Regular.woff2
│ │ ├── UniversalSans-Italic.woff
│ │ ├── UniversalSans-Italic.woff2
│ │ ├── UniversalSans-Regular.woff
│ │ ├── UniversalSans-Regular.woff2
│ │ └── stylesheet.css
│ ├── icon.svg
│ ├── flag.svg
│ ├── icons
│ │ └── connectors
│ │ │ ├── frame.svg
│ │ │ ├── coinbase.svg
│ │ │ ├── metamask.svg
│ │ │ └── walletconnect.svg
│ └── logo.svg
├── .env.mainnet
├── .env.sepolia
├── postcss.config.js
├── lib
│ ├── date.ts
│ ├── nation-token.ts
│ ├── network-id.ts
│ ├── date.test.ts
│ ├── use-handle-error.ts
│ ├── network-id.test.ts
│ ├── use-preferred-network.ts
│ ├── approve.ts
│ ├── passport-expiration.ts
│ ├── passport-expiration-hook.ts
│ ├── passport-expiration.test.ts
│ ├── sign-agreement.ts
│ ├── numbers.test.ts
│ ├── use-wagmi.ts
│ ├── numbers.ts
│ ├── connectors.ts
│ ├── static-call.ts
│ ├── balancer.ts
│ ├── passport-nft.ts
│ ├── config.ts
│ ├── ve-token.ts
│ └── liquidity-rewards.ts
├── cypress.config.ts
├── .env.goerli
├── next-env.d.ts
├── .eslintrc
├── jest.config.js
├── .prettierrc.json
├── components
│ ├── EthersInput.tsx
│ ├── Confetti.tsx
│ ├── PassportCheck.tsx
│ ├── PassportExpiration.tsx
│ ├── PreferredNetworkWrapper.tsx
│ ├── Balance.tsx
│ ├── HomeCard.tsx
│ ├── ErrorCard.tsx
│ ├── GradientLink.tsx
│ ├── Head.tsx
│ ├── MainCard.tsx
│ ├── TimeRange.tsx
│ ├── SwitchNetworkBanner.tsx
│ ├── ErrorProvider.tsx
│ ├── ActionButton.tsx
│ ├── ActionNeedsTokenApproval.tsx
│ └── Layout.tsx
├── styles
│ ├── globals.css
│ └── Home.module.css
├── .gitignore
├── cypress
│ └── e2e
│ │ ├── lock.cy.ts
│ │ └── app.cy.ts
├── pages
│ ├── _document.tsx
│ ├── api
│ │ └── store-signature.ts
│ ├── _app.tsx
│ ├── liquidity.tsx
│ ├── index.tsx
│ ├── citizen.tsx
│ ├── join.tsx
│ └── lock.tsx
├── tailwind.config.js
├── README.md
├── package.json
├── abis
│ └── ERC20.json
└── tsconfig.json
└── README.md
/contracts/deployments/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @nation3/developer-level-4
2 |
--------------------------------------------------------------------------------
/ui/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: 40..80
3 | round: down
4 | precision: 2
5 |
--------------------------------------------------------------------------------
/ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/favicon.ico
--------------------------------------------------------------------------------
/ui/public/social.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/social.jpg
--------------------------------------------------------------------------------
/ui/.env.mainnet:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHAIN=mainnet
2 | INFURA_ID=
3 | ALCHEMY_ID=
4 | ETHERSCAN_ID=
5 | NFTSTORAGE_KEY=
--------------------------------------------------------------------------------
/ui/public/passport/art3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/passport/art3.png
--------------------------------------------------------------------------------
/ui/public/passport/art4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/passport/art4.png
--------------------------------------------------------------------------------
/ui/.env.sepolia:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHAIN=sepolia
2 | INFURA_ID=
3 | ALCHEMY_ID=
4 | ETHERSCAN_ID=
5 | NFTSTORAGE_KEY=
6 |
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Black.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Bold.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Light.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff
--------------------------------------------------------------------------------
/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Medium.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff
--------------------------------------------------------------------------------
/ui/public/fonts/Bossa-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/Bossa-Regular.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Italic.woff2
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff
--------------------------------------------------------------------------------
/ui/public/fonts/UniversalSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artemd24/citizen-app/HEAD/ui/public/fonts/UniversalSans-Regular.woff2
--------------------------------------------------------------------------------
/ui/lib/date.ts:
--------------------------------------------------------------------------------
1 |
2 | export const dateToReadable = (date: Date | undefined): string | undefined => {
3 | if (!date) return undefined;
4 | return date.toISOString().substring(0, 10)
5 | }
6 |
--------------------------------------------------------------------------------
/ui/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | e2e: {
5 | baseUrl: 'http://localhost:42069',
6 | supportFile: false
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/ui/.env.goerli:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHAIN=goerli
2 | INFURA_ID=
3 | ALCHEMY_ID=
4 | ETHERSCAN_ID=
5 | NFTSTORAGE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDRlOTU2NDk3NjI4ZTdGMEQyODQxZWZkNEIwQ
--------------------------------------------------------------------------------
/ui/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/ui/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "prettier"],
3 | "rules": {
4 | "sort-imports": 0,
5 | "react/no-unescaped-entities": 0,
6 | "jsx-a11y/alt-text": [0],
7 | "react-hooks/exhaustive-deps": "error"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ui/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | coverageThreshold: {
6 | global: {
7 | lines: 11.00
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ui/lib/nation-token.ts:
--------------------------------------------------------------------------------
1 | import { nationToken } from '../lib/config'
2 | import { useBalance } from './use-wagmi'
3 |
4 | export function useNationBalance(address: any) {
5 | return useBalance({
6 | address: address,
7 | token: nationToken,
8 | watch: true,
9 | enabled: address,
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/ui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 2,
4 | "semi": false,
5 | "bracketSpacing": true,
6 | "jsxBracketSameLine": false,
7 | "arrowParens": "always",
8 | "importOrder": [
9 | "^.*/lib/.*$",
10 | "^.*/components/.*$",
11 | "^.*/pages/(?!.*.css$).*$",
12 | "^[./](?!.*.css$).*$",
13 | "^.*(css)$"
14 | ],
15 | "importOrderSeparation": false
16 | }
17 |
--------------------------------------------------------------------------------
/ui/public/passport/icon-plain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm" # See documentation for possible values
7 | directory: "/ui" # Location of package manifests
8 | schedule:
9 | interval: "daily"
10 | open-pull-requests-limit: 1
11 |
--------------------------------------------------------------------------------
/ui/lib/network-id.ts:
--------------------------------------------------------------------------------
1 | export function networkToId(network: any) {
2 | if (network == undefined) {
3 | return 1
4 | }
5 | switch (network.toLowerCase()) {
6 | case 'mainnet':
7 | return 1
8 | case 'ethereum':
9 | return 1
10 | case 'goerli':
11 | return 5
12 | case 'sepolia':
13 | return 11155111
14 | case 'local':
15 | return 31337
16 | default:
17 | return 1
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ui/components/EthersInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { isFixedDecimalsNumber } from '../lib/numbers'
3 |
4 | export default function EthersInput({ onChange, ...props }: any) {
5 | const onInputChange = (e: any) => {
6 | const value = e.target.value
7 | const isValid = isFixedDecimalsNumber(value)
8 | if (isValid) {
9 | onChange(value)
10 | }
11 | }
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/ui/lib/date.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { BigNumber } from 'ethers'
3 | import { dateToReadable } from './date'
4 |
5 | test("dateToReadable - February", () => {
6 | const actual = dateToReadable(new Date(2022, 1, 15, 23, 59))
7 | expect(actual).toBe('2022-02-15')
8 | })
9 |
10 | test("dateToReadable - December", () => {
11 | const actual = dateToReadable(new Date(2024, 11, 31, 23, 59))
12 | expect(actual).toBe('2024-12-31')
13 | })
14 |
--------------------------------------------------------------------------------
/ui/lib/use-handle-error.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useErrorContext } from '../components/ErrorProvider'
3 |
4 | // For some contract interactions, a reverted call is not an error
5 | export function useHandleError(object: any, throwOnRevert = true) {
6 | const {addError} = useErrorContext()
7 | useEffect(() => {
8 | if (throwOnRevert && object.error) {
9 | addError([object.error])
10 | }
11 | }, [object.error, throwOnRevert, addError])
12 | return object
13 | }
14 |
--------------------------------------------------------------------------------
/ui/lib/network-id.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { networkToId } from './network-id'
3 |
4 | test("networkToId goerli", () => {
5 | const actual = networkToId('goerli')
6 | expect(actual).toBe(5)
7 | })
8 |
9 | test("networkToId sepolia", () => {
10 | const actual = networkToId('sepolia')
11 | expect(actual).toBe(11155111)
12 | })
13 |
14 | test("networkToId mainnet", () => {
15 | const actual = networkToId('mainnet')
16 | expect(actual).toBe(1)
17 | })
18 |
--------------------------------------------------------------------------------
/ui/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url('/fonts/stylesheet.css');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | html,
8 | body {
9 | padding: 0;
10 | margin: 0;
11 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
12 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
13 | /* @apply font-body; */
14 | }
15 |
16 | a {
17 | color: inherit;
18 | text-decoration: none;
19 | }
20 |
21 | * {
22 | box-sizing: border-box;
23 | }
24 |
--------------------------------------------------------------------------------
/ui/components/Confetti.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useState, useEffect } from 'react'
3 | import Confetti from 'react-confetti'
4 | import { useTimeout } from 'react-use'
5 |
6 | export default function ConfettiComponent() {
7 | const [width, setWidth] = useState(0)
8 | const [height, setHeight] = useState(0)
9 | const [isComplete] = useTimeout(5000)
10 |
11 | useEffect(() => {
12 | setWidth(window.innerWidth)
13 | setHeight(window.innerHeight)
14 | }, [])
15 |
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # cypress
38 | cypress/screenshots
39 | cypress/videos
40 |
--------------------------------------------------------------------------------
/ui/cypress/e2e/lock.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Lock tokens', () => {
2 |
3 | it('lock amount: type more than 18 decimals', () => {
4 | cy.visit('/lock')
5 |
6 | // Expect empty value
7 | cy.get('#lockAmount')
8 | .invoke('val')
9 | .then(val => {
10 | expect(val).to.equal('')
11 | })
12 |
13 | // Type 19 decimals
14 | cy.get('#lockAmount').type('0.1919191919191919191')
15 |
16 | // Expect 18 decimals
17 | cy.get('#lockAmount')
18 | .invoke('val')
19 | .then(val => {
20 | expect(val).to.equal('0.191919191919191919')
21 | })
22 | })
23 | })
24 |
25 | export {}
26 |
--------------------------------------------------------------------------------
/ui/components/PassportCheck.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useHasPassport } from '../lib/passport-nft'
3 |
4 | export default function PassportCheck({
5 | children,
6 | address,
7 | onPassportChecked,
8 | }: React.PropsWithChildren<{
9 | address: string
10 | onPassportChecked: (hasPassport: boolean) => void
11 | }>) {
12 | const { hasPassport, isLoading: hasPassportLoading } = useHasPassport(address)
13 | useEffect(() => {
14 | if (!hasPassportLoading) {
15 | onPassportChecked(hasPassport)
16 | }
17 | }, [hasPassport, hasPassportLoading, onPassportChecked])
18 |
19 | return <>{children}>
20 | }
21 |
--------------------------------------------------------------------------------
/ui/components/PassportExpiration.tsx:
--------------------------------------------------------------------------------
1 | import { ClockIcon } from '@heroicons/react/24/outline'
2 | import { dateToReadable } from '../lib/date'
3 |
4 | interface Props {
5 | date: Date | undefined
6 | }
7 |
8 | export default function PassportExpiration({ date }: Props) {
9 | return (
10 |
11 |
12 |
13 |
14 |
Passport expiration date
15 |
16 | {!!date ? (date > new Date() ? dateToReadable(date) : 'Expired') : '-'}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/ui/components/PreferredNetworkWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import usePreferredNetwork from '../lib/use-preferred-network'
3 | import { useNetwork } from '../lib/use-wagmi'
4 | import SwitchNetworkBanner from './SwitchNetworkBanner'
5 |
6 | export default function PreferredNetworkWrapper({ children }: any) {
7 | const { chain: activeChain } = useNetwork()
8 | const { isPreferredNetwork, preferredNetwork } = usePreferredNetwork()
9 |
10 | return (
11 | <>
12 | {!isPreferredNetwork && activeChain?.id && (
13 |
14 | )}
15 | {children}
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/ui/components/Balance.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NumberType, transformNumber } from '../lib/numbers'
3 |
4 | export default function Balance({
5 | loading = false,
6 | balance,
7 | prefix = '',
8 | suffix = '',
9 | decimals = 2,
10 | }: any) {
11 | return (
12 | <>
13 | {loading ? (
14 |
15 | ) : balance ? (
16 | `${prefix}${transformNumber(
17 | balance,
18 | NumberType.string,
19 | decimals,
20 | )}${suffix}`
21 | ) : (
22 | 0
23 | )}
24 | >
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/ui/cypress/e2e/app.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Nation3 App UI', () => {
2 |
3 | it('should load the index page', () => {
4 | cy.visit('/')
5 | cy.get('h1').contains('Welcome to Nation3')
6 | }),
7 |
8 | it('should load the /join page', () => {
9 | cy.visit('/join')
10 | cy.get('h2').contains('Become a Nation3 citizen')
11 | }),
12 |
13 | it('should load the /lock page', () => {
14 | cy.visit('/lock')
15 | cy.get('h2').contains('Lock $NATION to get $veNATION')
16 | }),
17 |
18 | it('should load the /liquidity page', () => {
19 | cy.visit('/liquidity')
20 | cy.get('h2').contains('$NATION liquidity rewards')
21 | })
22 | })
23 |
24 | export {}
25 |
--------------------------------------------------------------------------------
/ui/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/contracts/deployments/sepolia.json:
--------------------------------------------------------------------------------
1 | {
2 | "nationToken": "0x23Ca3002706b71a440860E3cf8ff64679A00C9d7",
3 | "veNationToken": "0x8100e77899C24b0F7B516153F84868f850C034BF",
4 | "balancerLPToken": "0x6417755C00d5c17DeC196Df00a6F151E448B1471",
5 | "lpRewardsContract": "0x5514cF24D2241Ecc6012306927eA8d74E052416D",
6 | "nationPassportNFT": "0x11f30642277A70Dab74C6fAF4170a8b340BE2f98",
7 | "nationPassportNFTIssuer": "0xdad32e13E73ce4155a181cA0D350Fee0f2596940",
8 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL",
9 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link"
10 | }
11 |
--------------------------------------------------------------------------------
/ui/public/flag.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Related GitHub Issue
4 |
5 |
6 |
7 | ## Screenshots (if appropriate):
8 |
9 |
10 |
11 | ## How Has This Been Tested?
12 |
13 |
14 |
15 | - [ ] Status checks pass (lint, build, test)
16 | - [ ] Works on Sepolia preview deployment
17 | - [ ] Works on Mainnet preview deployment
18 |
19 | ## Are There Admin Tasks?
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ui/lib/use-preferred-network.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { networkToId } from './network-id'
3 | import { useNetwork } from './use-wagmi'
4 |
5 | const preferredNetwork = process.env.NEXT_PUBLIC_CHAIN
6 |
7 | export default function usePreferredNetwork() {
8 | const { chain: activeChain } = useNetwork()
9 |
10 | const [isPreferredNetwork, setIsPreferredNetwork] = useState(false)
11 |
12 | useEffect(() => {
13 | if (activeChain?.id && activeChain?.id == networkToId(preferredNetwork)) {
14 | setIsPreferredNetwork(true)
15 | } else {
16 | setIsPreferredNetwork(false)
17 | }
18 | }, [activeChain?.id])
19 |
20 | return { isPreferredNetwork, preferredNetwork }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/frame.svg:
--------------------------------------------------------------------------------
1 |
2 | FrameLogo4
3 |
4 |
--------------------------------------------------------------------------------
/ui/lib/approve.ts:
--------------------------------------------------------------------------------
1 | import ERC20 from '../abis/ERC20.json'
2 | import { useContractRead } from './use-wagmi'
3 | import { useContractWrite } from './use-wagmi'
4 |
5 | export function useTokenAllowance({ token, address, spender }: any) {
6 | return useContractRead(
7 | {
8 | address: token,
9 | abi: ERC20.abi,
10 | watch: true,
11 | enabled: Boolean(token && address && spender),
12 | },
13 | 'allowance',
14 | [address, spender]
15 | )
16 | }
17 |
18 | export function useTokenApproval({ amountNeeded, token, spender }: any) {
19 | return useContractWrite(
20 | {
21 | address: token,
22 | abi: ERC20.abi,
23 | },
24 | 'approve',
25 | [spender, amountNeeded]
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/ui/components/HomeCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import GradientLink from '../components/GradientLink'
3 |
4 | export default function HomeCard({
5 | href,
6 | icon,
7 | title,
8 | children,
9 | linkText,
10 | }: any) {
11 | return (
12 |
13 |
14 |
15 | {icon}
16 |
{title}
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 |
3 | const fourYearsInSeconds = 31556926 * 4
4 | const maxLockPeriod = ethers.BigNumber.from(fourYearsInSeconds);
5 |
6 | // threshold <= (lock amount) * (lock end - expiration) / (max lock period)
7 | // threshold * (max lock period) / (lock amount) <= (lock end - expiration)
8 | // expiration <= (lock end) - threshold * (max lock period) / (lock amount)
9 | export function getPassportExpirationDate(lockAmount: ethers.BigNumber, lockEnd: ethers.BigNumber, threshold: ethers.BigNumber): Date | undefined {
10 | if (lockAmount.isZero()) return undefined;
11 | const expiration = lockEnd.sub(threshold.mul(maxLockPeriod).div(lockAmount));
12 | return new Date(expiration.mul(1000).toNumber());
13 | }
14 |
--------------------------------------------------------------------------------
/ui/components/ErrorCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useErrorContext } from './ErrorProvider'
3 |
4 | export default function ErrorCard({ error }: any) {
5 | const { removeError } = useErrorContext()
6 |
7 | return (
8 |
9 |
10 |
Oopsie
11 |
12 | {error?.message ||
13 | error?.reason ||
14 | error?.data?.message ||
15 | 'Unknown error'}
16 |
17 |
18 |
removeError(error.key)}
21 | >
22 | ✕
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/ui/components/GradientLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import React from 'react'
3 |
4 | export default function GradientLink({ text, href, textSize }: any) {
5 | if (href?.charAt(0) === '/') {
6 | return (
7 |
8 |
11 | {text} →
12 |
13 |
14 | )
15 | } else {
16 | return (
17 |
23 | {text} →
24 |
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration-hook.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { useMemo } from "react";
3 | import { getPassportExpirationDate } from "./passport-expiration";
4 | import { usePassportRevokeUnderBalance } from "./passport-nft";
5 | import { useAccount } from "./use-wagmi";
6 | import { useVeNationLock } from "./ve-token";
7 | export function usePassportExpirationDate(): Date | undefined {
8 | const { address } = useAccount()
9 | const { data: veNationLock } = useVeNationLock(address)
10 |
11 | const { data: threshold } = usePassportRevokeUnderBalance()
12 |
13 | return useMemo(() => {
14 | if (!veNationLock) {
15 | return undefined;
16 | }
17 |
18 | const [lockAmount, lockEnd]: [ethers.BigNumber, ethers.BigNumber] = veNationLock;
19 | return getPassportExpirationDate(lockAmount, lockEnd, threshold);
20 | }, [veNationLock, threshold]);
21 | }
22 |
--------------------------------------------------------------------------------
/ui/components/Head.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import React from 'react'
3 |
4 | export default function WebsiteHead({ title }: any) {
5 | const description = 'Citizen app for the citizens of Nation3'
6 | const image = 'https://app.nation3.org/social.jpg'
7 |
8 | return (
9 |
10 | Nation3 | {title}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/contracts/deployments/goerli.json:
--------------------------------------------------------------------------------
1 | {
2 | "nationToken": "0x333A4823466879eeF910A04D473505da62142069",
3 | "veNationToken": "0xF7deF1D2FBDA6B74beE7452fdf7894Da9201065d",
4 | "nationDropContracts": [
5 | "0xCBbd5a8871E6Cf995305e58168cEaFB0be512c67"
6 | ],
7 | "balancerVault": "0xba12222222228d8ba445958a75a0704d566bf2c8",
8 | "balancerPoolId": "0x6f57329d43f3de9ff39d4424576db920b55060b30002000000000000000000d7",
9 | "balancerLPToken": "0x6f57329d43f3de9ff39d4424576db920b55060b3",
10 | "lpRewardsContract": "0x5e9a1c70391711408e064104292ace9d0b9763b6",
11 | "nationPassportNFT": "0x51F728c58697aFf9582cFDe3cBD00EC83E9ae7FC",
12 | "nationPassportNFTIssuer": "0x8c16926819AB30B8b29A8E23F5C230d164337093",
13 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL",
14 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link"
15 | }
16 |
--------------------------------------------------------------------------------
/contracts/deployments/mainnet.json:
--------------------------------------------------------------------------------
1 | {
2 | "nationToken": "0x333A4823466879eeF910A04D473505da62142069",
3 | "veNationToken": "0xF7deF1D2FBDA6B74beE7452fdf7894Da9201065d",
4 | "nationDropContracts": [
5 | "0xcab2B7614351649870e4DCC3490Ab692bf3beD60",
6 | "0x0B8107EaCeC5e81Fe6E8594dB95E03CF50685f84"
7 | ],
8 | "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
9 | "balancerPoolId": "0x0bf37157d30dfe6f56757dcadff01aed83b08cd600020000000000000000019a",
10 | "balancerLPToken": "0x0BF37157d30dFe6f56757DCadff01AEd83b08cD6",
11 | "lpRewardsContract": "0x4f1e79793fD5F5805b285C3F29379b8056A4476B",
12 | "nationPassportNFT": "0x3337dac9F251d4E403D6030E18e3cfB6a2cb1333",
13 | "nationPassportNFTIssuer": "0x279c0b6bfCBBA977eaF4ad1B2FFe3C208aa068aC",
14 | "nationPassportAgreementStatement": "By claiming a Nation3 passport I agree to the terms defined in the following URL",
15 | "nationPassportAgreementURI": "https://bafkreiadlf3apu3u7blxw7t2yxi7oyumeuzhoasq7gqmcbaaycq342xq74.ipfs.dweb.link"
16 | }
17 |
--------------------------------------------------------------------------------
/.github/workflows/ui_mainnet.yml:
--------------------------------------------------------------------------------
1 | name: /ui (Mainnet) CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | defaults:
15 | run:
16 | working-directory: ui
17 |
18 | strategy:
19 | matrix:
20 | node-version: [18.x, 20.x]
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | cache: 'npm'
30 | cache-dependency-path: ui/yarn.lock
31 | - run: npm install --global yarn
32 | - run: echo $(yarn --version)
33 | - run: cp .env.mainnet .env.local
34 | - run: yarn install
35 | - run: yarn build
36 | - run: yarn lint
37 | - run: yarn e2e:headless
38 | - run: yarn test
39 | - run: yarn test:coverage
40 |
--------------------------------------------------------------------------------
/ui/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 | import React from 'react'
3 |
4 | class WebsiteDocument extends Document {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 | }
36 |
37 | export default WebsiteDocument
38 |
--------------------------------------------------------------------------------
/ui/public/icons/connectors/coinbase.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ui/lib/passport-expiration.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jest/globals';
2 | import { BigNumber } from 'ethers';
3 | import { getPassportExpirationDate } from './passport-expiration';
4 |
5 | test("returns undefined if no lock amount", () => {
6 | const date = getPassportExpirationDate(BigNumber.from(0), BigNumber.from(0), BigNumber.from(String(1.5 * 10 ** 18)));
7 | expect(date).toBe(undefined);
8 | })
9 |
10 | const inFourYears = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 4;
11 |
12 | test("returns past date if below threshold", () => {
13 | const date = getPassportExpirationDate(BigNumber.from(1).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18)));
14 | expect(date).not.toBe(undefined);
15 | expect(date!.getTime() < Date.now()).toBe(true);
16 | })
17 |
18 | test("returns future date if over threshold", () => {
19 | const date = getPassportExpirationDate(BigNumber.from(2).mul(String(10 ** 18)), BigNumber.from(inFourYears), BigNumber.from(String(1.5 * 10 ** 18)));
20 | expect(date).not.toBe(undefined);
21 | expect(date!.getTime() > Date.now()).toBe(true);
22 | })
23 |
--------------------------------------------------------------------------------
/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './pages/**/*.{js,ts,jsx,tsx}',
4 | './components/**/*.{js,ts,jsx,tsx}',
5 | ],
6 | theme: {
7 | fontFamily: {
8 | display: ['Poppins', 'sans-serif'],
9 | body: ['UniversalSans', 'sans-serif'],
10 | },
11 | fontWeight: {
12 | light: 200,
13 | normal: 300,
14 | medium: 400,
15 | semibold: 500,
16 | bold: 500,
17 | },
18 | extend: {
19 | colors: {
20 | n3blue: '#69C9FF',
21 | n3green: '#88F1BB',
22 | 'n3blue-100': '#DCFFFF',
23 | 'n3green-100': '#D5FFFF',
24 | n3bg: '#F4FAFF',
25 | n3nav: '#7395B2',
26 | },
27 | },
28 | },
29 | plugins: [require('daisyui')],
30 | daisyui: {
31 | themes: [
32 | {
33 | mytheme: {
34 | primary: '#69C9FF',
35 | secondary: '#88F1BB',
36 | accent: '#88F1BB',
37 | neutral: '#3d4451',
38 | 'primary-content': '#ffffff',
39 | 'base-100': '#ffffff',
40 | 'base-content': '#224059',
41 | },
42 | },
43 | ],
44 | },
45 | }
46 |
--------------------------------------------------------------------------------
/ui/components/MainCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function MainCard({
4 | children,
5 | title,
6 | loading,
7 | gradientBg,
8 | maxWidthClassNames,
9 | }: any) {
10 | return (
11 | <>
12 | {!loading ? (
13 |
18 |
23 |
24 |
29 | {title}
30 |
31 | {children}
32 |
33 |
34 |
35 | ) : (
36 |
37 | )}
38 | >
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/ui/components/TimeRange.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function TimeRange({
4 | time,
5 | min,
6 | max,
7 | displaySteps,
8 | onChange,
9 | }: any) {
10 | const step = (min * 100) / max
11 |
12 | return (
13 | <>
14 | {
20 | onChange(new Date(parseFloat(e.target.value)))
21 | }}
22 | className="range range-secondary mt-4"
23 | step={step || 0}
24 | />
25 |
26 |
27 | {displaySteps ? (
28 | <>
29 | -
30 |
31 | 1 y
32 |
33 | 2 y
34 |
35 | 3 y
36 | >
37 | ) : (
38 | <>
39 | {' '}
40 | {new Date(min).toISOString().substring(0, 10)}
41 |
42 |
43 |
44 | >
45 | )}
46 |
47 | 4 y
48 |
49 | >
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/ui_sepolia.yml:
--------------------------------------------------------------------------------
1 | name: /ui (Sepolia) CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | defaults:
15 | run:
16 | working-directory: ui
17 |
18 | strategy:
19 | matrix:
20 | node-version: [18.x, 20.x]
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | cache: 'npm'
30 | cache-dependency-path: ui/yarn.lock
31 | - run: npm install --global yarn
32 | - run: echo $(yarn --version)
33 | - run: cp .env.sepolia .env.local
34 | - run: yarn install
35 | - run: yarn build
36 | - run: yarn lint
37 | - run: yarn e2e:headless
38 | - run: yarn test
39 | - run: yarn test:coverage
40 | - name: Upload coverage reports to Codecov
41 | uses: codecov/codecov-action@v4.0.1
42 | with:
43 | token: ${{ secrets.CODECOV_TOKEN }}
44 | slug: nation3/citizen-app
45 |
--------------------------------------------------------------------------------
/ui/lib/sign-agreement.ts:
--------------------------------------------------------------------------------
1 | import {
2 | nationPassportNFTIssuer,
3 | nationPassportAgreementStatement,
4 | nationPassportAgreementURI
5 | } from './config'
6 | import { networkToId } from './network-id'
7 | import { useSignTypedData } from './use-wagmi'
8 |
9 | export const domain = {
10 | name: 'PassportIssuer',
11 | version: '1',
12 | chainId: networkToId(process.env.NEXT_PUBLIC_CHAIN),
13 | verifyingContract: nationPassportNFTIssuer,
14 | }
15 |
16 | export const types = {
17 | Agreement: [
18 | { name: 'statement', type: 'string' },
19 | { name: 'termsURI', type: 'string' },
20 | ],
21 | }
22 |
23 | export const value = {
24 | statement: `${nationPassportAgreementStatement}`,
25 | termsURI: `${nationPassportAgreementURI}`,
26 | }
27 |
28 | export function useSignAgreement({ onSuccess }: { onSuccess: Function }) {
29 | return useSignTypedData({
30 | domain,
31 | types,
32 | value,
33 | onSuccess,
34 | })
35 | }
36 |
37 | export async function storeSignature(signature: string, tx: string) {
38 | const response = await fetch('/api/store-signature', {
39 | method: 'POST',
40 | headers: {
41 | 'Content-Type': 'application/json',
42 | },
43 | body: JSON.stringify({ signature, tx }),
44 | })
45 | return response.json()
46 | }
47 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | # Nation3 Citizen App UI
2 |
3 | https://app.nation3.org
4 |
5 | ## Run the UI locally
6 |
7 | Navigate to the folder of the UI app:
8 | ```
9 | cd ui/
10 | ```
11 |
12 | Install the dependencies:
13 | ```
14 | yarn install
15 | ```
16 |
17 | Add variables to your local development environment:
18 | ```
19 | cp .env.mainnet .env.local
20 | ```
21 | or
22 | ```
23 | cp .env.goerli .env.local
24 | ```
25 | or
26 | ```
27 | cp .env.sepolia .env.local
28 | ```
29 |
30 |
31 | Build:
32 | ```
33 | yarn build
34 | ```
35 |
36 | Lint:
37 | ```
38 | yarn lint
39 | ```
40 |
41 | Start the development server:
42 | ```
43 | yarn dev
44 | ```
45 |
46 | Then open http://localhost:42069 in a browser.
47 |
48 | ## Integration Testing
49 |
50 | Run the integration tests:
51 | ```
52 | yarn cypress
53 | ```
54 |
55 | Run the integration tests headlessly:
56 | ```
57 | yarn cypress:headless
58 | ```
59 |
60 | ## Unit Testing
61 |
62 | Run unit tests:
63 | ```
64 | yarn test
65 | ```
66 |
67 | ## Code Coverage
68 |
69 | [](https://codecov.io/gh/nation3/citizen-app)
70 |
71 | [](https://codecov.io/gh/nation3/citizen-app)
72 |
73 | Run code coverage:
74 | ```
75 | yarn test:coverage
76 | ```
77 |
--------------------------------------------------------------------------------
/ui/public/passport/ballot.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/components/SwitchNetworkBanner.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
2 | import React from 'react'
3 | import { useNetwork } from '../lib/use-wagmi'
4 |
5 | const chainIds = {
6 | mainnet: 1,
7 | goerli: 5,
8 | }
9 |
10 | type Indexable = {
11 | [key: string]: any
12 | }
13 |
14 | export default function SwitchNetworkBanner({ newNetwork }: any) {
15 | const { switchNetwork } = useNetwork()
16 |
17 | const capitalized = (network: any) =>
18 | network.charAt(0).toUpperCase() + network.slice(1)
19 |
20 | return (
21 |
22 |
23 |
24 | Nation3 uses {capitalized(newNetwork)} as its preferred network.
25 |
26 |
27 | {/* There's a small chance the wallet used won't support switchNetwork, in which case the user needs to manually switch */}
28 | {/* Also check if we have specified a chain ID for the network */}
29 | {switchNetwork && (chainIds as Indexable)[newNetwork] && (
30 |
switchNetwork((chainIds as Indexable)[newNetwork])}
33 | >
34 | Switch to {capitalized(newNetwork)}
35 |
36 | )}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/ui/lib/numbers.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from '@jest/globals'
2 | import { BigNumber } from 'ethers'
3 | import { NumberType, transformNumber, isFixedDecimalsNumber } from './numbers'
4 |
5 | describe('transformNumber', () => {
6 | test("transformNumber to Number", () => {
7 | const actual = transformNumber(333, NumberType.number)
8 | expect(actual).toBe(Number(333))
9 | })
10 |
11 | test("transformNumber to BigNumber", () => {
12 | const actual = transformNumber(333, NumberType.bignumber)
13 | expect(actual).toBeInstanceOf(BigNumber)
14 | })
15 |
16 | test("transformNumber to String", () => {
17 | const actual = transformNumber(333, NumberType.string)
18 | expect(actual).toBe("333.000000000000000000")
19 | })
20 |
21 | test("transformNumber to String (2 decimals)", () => {
22 | const actual = transformNumber(333, NumberType.string, 2)
23 | expect(actual).toBe("333.00")
24 | })
25 | })
26 |
27 | describe('isFixedDecimalsNumber', () => {
28 | test('Is working for integers', () => {
29 | const isValid = isFixedDecimalsNumber(12345);
30 | expect(isValid).toBe(true)
31 | })
32 |
33 | test('Fails for too long decimals', () => {
34 | const isValid = isFixedDecimalsNumber('1.111111111111111111111111111111');
35 | expect(isValid).toBe(false)
36 | })
37 |
38 | test('Check "decimals" param is working properly', ()=> {
39 | const isValid = isFixedDecimalsNumber(1.12345, 2);
40 | expect(isValid).toBe(false)
41 | })
42 | })
--------------------------------------------------------------------------------
/ui/pages/api/store-signature.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 | import { NFTStorage, Blob } from 'nft.storage'
4 | import { nationPassportNFTIssuer } from '../../lib/config'
5 | import { provider } from '../../lib/connectors'
6 | import { domain, types, value } from '../../lib/sign-agreement'
7 |
8 | const client = new NFTStorage({
9 | token: process.env.NFTSTORAGE_KEY || '',
10 | })
11 |
12 | export default async function storeSignature(
13 | req: NextApiRequest,
14 | res: NextApiResponse
15 | ) {
16 | try {
17 | const prov = provider()
18 |
19 | const sender = ethers.utils.verifyTypedData(
20 | domain,
21 | types,
22 | value,
23 | req.body.signature
24 | )
25 | const { from, to, confirmations } = await prov.getTransaction(req.body.tx)
26 | // To prevent spam attacks: Check that the sender of the transaction is the same as the uploader,
27 | // the receiver is the passport issuer and the transaction is not very old
28 | if (
29 | sender === from &&
30 | to === nationPassportNFTIssuer &&
31 | confirmations < 14 * 60
32 | ) {
33 | const data = new Blob([
34 | JSON.stringify({ v: 1, sig: req.body.signature, tx: req.body.tx }),
35 | ])
36 | const cid = await client.storeBlob(data)
37 | res.status(200).json({ cid })
38 | }
39 | } catch (e) {
40 | console.error(e)
41 | res.status(500).json({
42 | error: `Storing your signature failed. This is your signature: ${req.body.signature}`,
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ui/lib/use-wagmi.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useConnect as _useConnect,
3 | useAccount as _useAccount,
4 | useBalance as _useBalance,
5 | useNetwork as _useNetwork,
6 | useContractRead as _useContractRead,
7 | useContractWrite as _useContractWrite,
8 | useSignTypedData as _useSignTypedData,
9 | } from 'wagmi'
10 | import { useStaticCall as _useStaticCall } from './static-call'
11 | import { useHandleError } from './use-handle-error'
12 |
13 | export function useConnect() {
14 | return useHandleError(_useConnect())
15 | }
16 |
17 | // custom extension of wagmi
18 | export function useStaticCall(params: any) {
19 | return useHandleError(_useStaticCall(params))
20 | }
21 |
22 | export function useAccount(params?: any) {
23 | return useHandleError(_useAccount(params))
24 | }
25 |
26 | export function useNetwork() {
27 | return useHandleError(_useNetwork())
28 | }
29 |
30 | export function useBalance(params: any) {
31 | return useHandleError(_useBalance(params))
32 | }
33 |
34 | export function useContractRead(
35 | config: any,
36 | functionName: any,
37 | args?: any,
38 | overrides?: any,
39 | throwOnRevert?: any
40 | ) {
41 | return useHandleError(
42 | _useContractRead({ ...config, functionName, args, overrides }),
43 | throwOnRevert
44 | )
45 | }
46 |
47 | export function useContractWrite(
48 | config: any,
49 | functionName: any,
50 | args: any,
51 | overrides?: any,
52 | throwOnRevert?: any
53 | ) {
54 | return useHandleError(
55 | _useContractWrite({ ...config, functionName, args, overrides }),
56 | throwOnRevert
57 | )
58 | }
59 |
60 | export function useSignTypedData(params: any) {
61 | return useHandleError(_useSignTypedData(params))
62 | }
63 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "citizen-app",
3 | "private": true,
4 | "scripts": {
5 | "dev": "PORT=42069 next dev",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint",
9 | "cypress": "cypress open",
10 | "cypress:headless": "cypress run",
11 | "e2e": "start-server-and-test dev http://localhost:42069 cypress",
12 | "e2e:headless": "start-server-and-test dev http://localhost:42069 cypress:headless",
13 | "test": "jest",
14 | "test:coverage": "jest --coverage --collectCoverageFrom=./lib/**"
15 | },
16 | "dependencies": {
17 | "@heroicons/react": "^2.0.11",
18 | "@metamask/eth-sig-util": "^7.0.1",
19 | "@types/node": "^20.10.4",
20 | "@types/react-dom": "^18.0.4",
21 | "daisyui": "^2.13.5",
22 | "ethers": "^5.7.1",
23 | "next": "12.1.0",
24 | "nft.storage": "^7.1.1",
25 | "react": "17.0.2",
26 | "react-animated-3d-card": "^1.0.2",
27 | "react-blockies": "^1.4.1",
28 | "react-confetti": "^6.0.1",
29 | "react-dom": "17.0.2",
30 | "react-use": "^17.5.0",
31 | "typescript": "^4.7.4",
32 | "use-nft": "^0.12.0",
33 | "wagmi": "0.12.8"
34 | },
35 | "devDependencies": {
36 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
37 | "@types/jest": "^28.1.8",
38 | "@types/react": "18.0.1",
39 | "autoprefixer": "^10.4.0",
40 | "cypress": "^10.7.0",
41 | "eslint": "8.21.0",
42 | "eslint-config-next": "12.0.7",
43 | "eslint-config-prettier": "^8.3.0",
44 | "jest": "^28.1.3",
45 | "postcss": "^8.4.5",
46 | "prettier": "^3.2.5",
47 | "start-server-and-test": "^1.14.0",
48 | "tailwindcss": "^3.1.8",
49 | "ts-jest": "^28.0.8"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/ui/public/fonts/stylesheet.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Bossa';
3 | src: url('Bossa-Black.woff2') format('woff2'),
4 | url('Bossa-Black.woff') format('woff');
5 | font-weight: 900;
6 | font-style: normal;
7 | font-display: swap;
8 | }
9 |
10 | @font-face {
11 | font-family: 'Bossa';
12 | src: url('Bossa-Regular.woff2') format('woff2'),
13 | url('Bossa-Regular.woff') format('woff');
14 | font-weight: normal;
15 | font-style: normal;
16 | font-display: swap;
17 | }
18 |
19 | @font-face {
20 | font-family: 'Bossa';
21 | src: url('Bossa-Light.woff2') format('woff2'),
22 | url('Bossa-Light.woff') format('woff');
23 | font-weight: 300;
24 | font-style: normal;
25 | font-display: swap;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Bossa';
30 | src: url('Bossa-Medium.woff2') format('woff2'),
31 | url('Bossa-Medium.woff') format('woff');
32 | font-weight: 500;
33 | font-style: normal;
34 | font-display: swap;
35 | }
36 |
37 | @font-face {
38 | font-family: 'Bossa';
39 | src: url('Bossa-Bold.woff2') format('woff2'),
40 | url('Bossa-Bold.woff') format('woff');
41 | font-weight: bold;
42 | font-style: normal;
43 | font-display: swap;
44 | }
45 |
46 | @font-face {
47 | font-family: 'UniversalSans';
48 | src: url('UniversalSans-Italic.woff2') format('woff2'),
49 | url('UniversalSans-Italic.woff') format('woff');
50 | font-weight: normal;
51 | font-style: italic;
52 | font-display: swap;
53 | }
54 |
55 | @font-face {
56 | font-family: 'UniversalSans';
57 | src: url('UniversalSans-Regular.woff2') format('woff2'),
58 | url('UniversalSans-Regular.woff') format('woff');
59 | font-weight: normal;
60 | font-style: normal;
61 | font-display: swap;
62 | }
63 |
--------------------------------------------------------------------------------
/ui/lib/numbers.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber, ethers } from 'ethers'
2 |
3 | function stringToNumber(string: any, decimals: any) {
4 | return Number(string).toFixed(decimals)
5 | }
6 |
7 | export enum NumberType {
8 | number = 'number',
9 | bignumber = 'bignumber',
10 | string = 'string',
11 | }
12 |
13 | export function transformNumber(
14 | num: number | BigNumber | string,
15 | to: NumberType,
16 | decimals = 18
17 | ): BigNumber | string | number {
18 | if (!num) {
19 | return to === NumberType.bignumber ? ethers.BigNumber.from('0') : 0
20 | }
21 |
22 | if (to === NumberType.bignumber) {
23 | if (num instanceof ethers.BigNumber) return num
24 |
25 | return ethers.utils.parseUnits(
26 | typeof num === 'string' ? num : num.toString(),
27 | decimals
28 | )
29 | } else if (to === NumberType.number) {
30 | if (typeof num === 'number') return num
31 |
32 | if (num instanceof ethers.BigNumber) {
33 | return stringToNumber(ethers.utils.formatUnits(num, 18), decimals)
34 | } else if (typeof num === 'string') {
35 | return parseFloat(num).toFixed(decimals)
36 | }
37 | } else if (to === NumberType.string) {
38 | if (typeof num === 'string') return num
39 |
40 | if (num instanceof ethers.BigNumber) {
41 | return stringToNumber(
42 | ethers.utils.formatUnits(num, 18),
43 | decimals
44 | ).toString()
45 | } else if (typeof num === 'number') {
46 | return num.toFixed(decimals).toString()
47 | }
48 | }
49 | return 0
50 | }
51 |
52 | export function isFixedDecimalsNumber(value: any, decimals = 18) {
53 | const NUMBER_REGEX = RegExp(`^(\\d*\\.{0,1}\\d{0,${decimals}}$)`)
54 | const isValid = value.toString().match(NUMBER_REGEX)
55 |
56 | return Boolean(isValid);
57 | }
58 |
--------------------------------------------------------------------------------
/ui/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { useEffect, useState } from 'react'
3 | import React from 'react'
4 | import { NftProvider } from 'use-nft'
5 | import { WagmiConfig, createClient } from 'wagmi'
6 | import { connectors, provider as externalProvider } from '../lib/connectors'
7 | import { ErrorProvider } from '../components/ErrorProvider'
8 | import Layout from '../components/Layout'
9 | import '../styles/globals.css'
10 |
11 | function App({ Component, pageProps }: any) {
12 | const [client, setClient] = useState()
13 |
14 | useEffect(() => {
15 | let provider = externalProvider
16 |
17 | const userProvider =
18 | window.ethereum || (window as unknown as any).web3?.currentProvider
19 | if (userProvider && process.env.NEXT_PUBLIC_CHAIN !== 'local') {
20 | provider = () => {
21 | console.log(
22 | `Provider: Connected to the user's provider on chain with ID ${parseInt(
23 | userProvider.networkVersion,
24 | )}`,
25 | )
26 | return new ethers.providers.Web3Provider(
27 | userProvider,
28 | process.env.NEXT_PUBLIC_CHAIN,
29 | )
30 | }
31 | }
32 | setClient(
33 | createClient({
34 | autoConnect: true,
35 | connectors,
36 | provider: provider(),
37 | }),
38 | )
39 | }, [])
40 |
41 | return (
42 | <>
43 | {client && (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | )}
54 | >
55 | )
56 | }
57 |
58 | export default App
59 |
--------------------------------------------------------------------------------
/ui/public/passport/contactless.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/lib/connectors.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { mainnet, goerli, configureChains } from 'wagmi'
3 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet'
4 | import { InjectedConnector } from 'wagmi/connectors/injected'
5 | import { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect'
6 | import CoinbaseWalletIcon from '../public/icons/connectors/coinbase.svg'
7 | import FrameIcon from '../public/icons/connectors/frame.svg'
8 | import MetaMaskIcon from '../public/icons/connectors/metamask.svg'
9 | import WalletConnectIcon from '../public/icons/connectors/walletconnect.svg'
10 | import { networkToId } from './network-id'
11 |
12 | const chains = [mainnet, goerli]
13 |
14 | export function provider() {
15 | if (process.env.NEXT_PUBLIC_CHAIN === 'local') {
16 | console.log('Provider: Connected to localhost provider')
17 | return new ethers.providers.JsonRpcProvider(
18 | 'http://127.0.0.1:7545',
19 | networkToId(process.env.NEXT_PUBLIC_CHAIN)
20 | )
21 | } else {
22 | console.log(
23 | `Provider: Connected to the external provider on chain ${process.env.NEXT_PUBLIC_CHAIN}`
24 | )
25 | return ethers.getDefaultProvider(process.env.NEXT_PUBLIC_CHAIN, {
26 | infura: process.env.NEXT_PUBLIC_INFURA_ID,
27 | alchemy: process.env.NEXT_PUBLIC_ALCHEMY_ID,
28 | quorum: 1,
29 | });
30 | }
31 | }
32 |
33 | export const connectors = [
34 | new InjectedConnector({
35 | chains,
36 | options: { shimDisconnect: true },
37 | }),
38 | new WalletConnectConnector({
39 | chains,
40 | options: {
41 | showQrModal: true,
42 | projectId: 'de21254f0716238419606243642a9266',
43 | },
44 | }),
45 | new CoinbaseWalletConnector({
46 | chains,
47 | options: {
48 | appName: 'Nation3 app',
49 | },
50 | }),
51 | ]
52 |
53 | export const connectorIcons = {
54 | Frame: FrameIcon,
55 | MetaMask: MetaMaskIcon,
56 | WalletConnect: WalletConnectIcon,
57 | 'Coinbase Wallet': CoinbaseWalletIcon,
58 | }
--------------------------------------------------------------------------------
/ui/lib/static-call.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react'
2 | import { useContract, useSigner } from 'wagmi'
3 |
4 | // Hook for calling a contract write method without executing, getting the result without changing state (and thus gas fees)
5 | export function useStaticCall({
6 | address,
7 | abi,
8 | methodName,
9 | args,
10 | defaultData,
11 | skip = false,
12 |
13 | // set to false if you'd like a reverted txn to be ignored and use the default data
14 | throwOnRevert = true,
15 |
16 | genericErrorMessage = 'Error calling contract',
17 | }: any) {
18 | args = args?.length === 0 ? undefined : args // args should be undefined if there are no arguments, make sure here
19 |
20 | const [loading, setLoading] = useState(true)
21 | const [error, setError] = useState(null)
22 | const [data, setData] = useState(defaultData)
23 |
24 | const { data: signer } = useSigner()
25 |
26 | const contract = useContract({
27 | address,
28 | abi,
29 | signerOrProvider: signer,
30 | })!
31 |
32 | const stringifiedArgs = useMemo(() => JSON.stringify(args), [args]);
33 | useEffect(() => {
34 | const call = async () => {
35 | if (skip || !signer) {
36 | return
37 | }
38 | try {
39 | const result = args
40 | ? await contract.callStatic[methodName](...args)
41 | : await contract.callStatic[methodName]()
42 | setData(result)
43 | } catch (error) {
44 | if (throwOnRevert) {
45 | console.error(error)
46 | setError({ ...(error as any), message: genericErrorMessage })
47 | }
48 | } finally {
49 | setLoading(false)
50 | }
51 | }
52 |
53 | call()
54 | // eslint-disable-next-line react-hooks/exhaustive-deps
55 | }, [
56 | skip,
57 | address,
58 | abi,
59 | methodName,
60 | genericErrorMessage,
61 | signer,
62 | stringifiedArgs,
63 | ])
64 |
65 | return {
66 | data,
67 | error,
68 | loading,
69 | method: contract.callStatic[methodName],
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/ui/lib/balancer.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import BalancerVault from '../abis/BalancerVault.json'
3 | import { balancerVault } from './config'
4 | import { NumberType, transformNumber } from './numbers'
5 | import { useContractRead } from './use-wagmi'
6 |
7 | export function useBalancerPool(id: any) {
8 | const { data: poolData, isLoading } = useContractRead(
9 | {
10 | address: balancerVault,
11 | abi: BalancerVault.abi,
12 | },
13 | 'getPoolTokens',
14 | [id],
15 | undefined,
16 | process.env.NEXT_PUBLIC_CHAIN === 'mainnet'
17 | )
18 |
19 | const [poolValue, setPoolValue] = useState(0)
20 | const [nationPrice, setNationPrice] = useState(0)
21 | const [ethPrice, setEthPrice] = useState(0)
22 |
23 | useEffect(() => {
24 | async function fetchData() {
25 | const priceRes = await fetch(
26 | 'https://api.coingecko.com/api/v3/simple/price?ids=nation3,ethereum&vs_currencies=usd'
27 | )
28 | const { nation3, ethereum } = await priceRes.json()
29 | setNationPrice(nation3.usd)
30 | setEthPrice(ethereum.usd)
31 | }
32 | fetchData()
33 | }, [])
34 |
35 | useEffect(() => {
36 | if (!isLoading && poolData && nationPrice && ethPrice) {
37 | let nationBalance
38 | let wethBalance
39 | if (process.env.NEXT_PUBLIC_CHAIN === 'mainnet') {
40 | const balances = poolData[1]
41 | nationBalance = balances[0]
42 | wethBalance = balances[1]
43 | } else {
44 | nationBalance = transformNumber(333, NumberType.bignumber)
45 | wethBalance = transformNumber(333, NumberType.bignumber)
46 | }
47 |
48 | if (nationBalance && wethBalance) {
49 | const nationValue = nationBalance.mul(Math.round(nationPrice))
50 | const ethValue = wethBalance.mul(Math.round(ethPrice))
51 | const totalValue = nationValue.add(ethValue)
52 | setPoolValue(totalValue)
53 | }
54 | }
55 | }, [isLoading, poolData, ethPrice, nationPrice])
56 | return { poolValue, nationPrice, isLoading }
57 | }
58 |
--------------------------------------------------------------------------------
/ui/public/passport/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/components/ErrorProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useCallback, useContext, useState } from 'react'
2 | import { useNetwork } from 'wagmi'
3 | import { networkToId } from '../lib/network-id'
4 |
5 | const ErrorContext = createContext({} as any)
6 |
7 | function ErrorProvider({ children }: any) {
8 | const [errors, setErrors] = useState([] as any[])
9 | let [count, setCount] = useState(0)
10 | const { chain: activeChain } = useNetwork()
11 |
12 | const addError = useCallback(
13 | (newErrors: any) => {
14 | if (
15 | newErrors &&
16 | newErrors[0] &&
17 | activeChain?.id &&
18 | activeChain.id == networkToId(process.env.NEXT_PUBLIC_CHAIN)
19 | ) {
20 | for (let error of newErrors) {
21 | console.error(error)
22 | if (error instanceof Error) {
23 | error = JSON.parse(
24 | JSON.stringify(error, Object.getOwnPropertyNames(error)),
25 | )
26 | // Don't add the error if it's "invalid address or ENS name",
27 | // no idea why those errors appear in the first place.
28 | if (
29 | error.code &&
30 | (error.code === 'INVALID_ARGUMENT' ||
31 | error.code === 'MISSING_ARGUMENT')
32 | ) {
33 | return
34 | }
35 | }
36 | setErrors((prev) => [...prev, { key: prev.length, ...error }])
37 | setCount((prev) => prev + 1)
38 | }
39 | }
40 | },
41 | [activeChain?.id],
42 | )
43 |
44 | const removeError = (key: any) => {
45 | if (key > -1) {
46 | setErrors(errors.filter((error: any) => error.key !== key))
47 | setCount(--count)
48 | }
49 | }
50 |
51 | const context = { errors, addError, removeError }
52 | return (
53 | {children}
54 | )
55 | }
56 |
57 | function useErrorContext() {
58 | const errors = useContext(ErrorContext)
59 | if (errors === undefined) {
60 | throw new Error('useErrorContext must be used within a ErrorProvider')
61 | }
62 | return errors
63 | }
64 |
65 | export { ErrorProvider, useErrorContext }
66 |
--------------------------------------------------------------------------------
/ui/lib/passport-nft.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useNft } from 'use-nft'
3 | import PassportIssuer from '../abis/PassportIssuer.json'
4 | import { nationPassportNFT, nationPassportNFTIssuer } from './config'
5 | import { useContractRead, useContractWrite } from './use-wagmi'
6 |
7 | const nftIssuerContractParams = {
8 | address: nationPassportNFTIssuer,
9 | abi: PassportIssuer.abi,
10 | }
11 |
12 | export function useHasPassport(address: any) {
13 | const [hasPassport, setHasPassport] = useState(false)
14 | const { data: passportStatus, isLoading } = usePassportStatus(address)
15 |
16 | useEffect(() => {
17 | if (passportStatus == 1) {
18 | setHasPassport(true)
19 | }
20 | }, [passportStatus, isLoading])
21 |
22 | return { hasPassport, isLoading }
23 | }
24 |
25 | export function usePassportStatus(address: any) {
26 | return useContractRead(
27 | {
28 | ...nftIssuerContractParams,
29 | watch: true,
30 | enabled: Boolean(address),
31 | },
32 | 'passportStatus',
33 | [address],
34 | undefined,
35 | false
36 | )
37 | }
38 |
39 | export function useClaimRequiredBalance() {
40 | return useContractRead(
41 | {
42 | ...nftIssuerContractParams,
43 | watch: true,
44 | },
45 | 'claimRequiredBalance',
46 | undefined,
47 | )
48 | }
49 |
50 | export function useClaimPassport() {
51 | return useContractWrite(
52 | {
53 | address: nationPassportNFTIssuer,
54 | abi: PassportIssuer.abi,
55 | },
56 | 'claim',
57 | undefined
58 | )
59 | }
60 |
61 | export function usePassport(address: any) {
62 | const { data: id, isLoading: loadingID } = useContractRead(
63 | {
64 | ...nftIssuerContractParams,
65 | enable: Boolean(address)
66 | },
67 | 'passportId',
68 | [address],
69 | undefined,
70 | false
71 | )
72 | console.log(`Passport ID ${id}`)
73 | const { loading, nft } = useNft(nationPassportNFT || '', id)
74 | return { data: { id, nft }, isLoading: loadingID || loading }
75 | }
76 |
77 | export function usePassportRevokeUnderBalance() {
78 | return useContractRead(
79 | {
80 | ...nftIssuerContractParams,
81 | },
82 | 'revokeUnderBalance',
83 | undefined
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/ui/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/ui/components/ActionButton.tsx:
--------------------------------------------------------------------------------
1 | import { BigNumber } from 'ethers'
2 | import { ReactNode } from 'react'
3 | import { useWaitForTransaction } from 'wagmi'
4 | import usePreferredNetwork from '../lib/use-preferred-network'
5 | import { useAccount, useNetwork } from '../lib/use-wagmi'
6 | import ActionNeedsTokenApproval from './ActionNeedsTokenApproval'
7 |
8 | export interface ActionButtonProps {
9 | className?: string
10 | children: ReactNode
11 | action: any
12 | preAction?: Function
13 | postAction?: Function
14 | approval?: {
15 | token: string
16 | spender: string
17 | amountNeeded: BigNumber | number | string
18 | approveText: string
19 | allowUnlimited?: boolean
20 | }
21 | }
22 |
23 | export default function ActionButton({
24 | className,
25 | children,
26 | action,
27 | preAction,
28 | postAction,
29 | approval,
30 | }: ActionButtonProps) {
31 | const { address } = useAccount()
32 | const { writeAsync, data, isLoadingOverride } = action
33 | const { isLoading } = useWaitForTransaction({
34 | hash: data?.hash,
35 | })
36 | const onClick = async () => {
37 | preAction && preAction()
38 | const tx = await writeAsync()
39 | tx?.wait && (await tx.wait())
40 | postAction && postAction()
41 | }
42 |
43 | const { chain: activeChain } = useNetwork()
44 | const { isPreferredNetwork } = usePreferredNetwork()
45 |
46 | return (
47 | <>
48 | {!isPreferredNetwork ? (
49 |
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 || "goerli";
34 | const defaultConfig = require(`../../contracts/deployments/${chain}.json`) as DeploymentConfig
35 | const config: Config = {
36 | nationToken: defaultConfig.nationToken || zeroAddress,
37 | veNationToken: defaultConfig.veNationToken || zeroAddress,
38 | balancerDomain: chain === 'mainnet' ? 'https://app.balancer.fi/#/ethereum' : `https://app.balancer.fi/#/${chain}`,
39 | balancerVault: defaultConfig.balancerVault || zeroAddress,
40 | balancerPoolId: defaultConfig.balancerPoolId || zeroAddress,
41 | balancerLPToken: defaultConfig.balancerLPToken || zeroAddress,
42 | etherscanDomain: chain === 'mainnet' ? 'https://etherscan.io' : `https://${chain}.etherscan.io`,
43 | lpRewardsContract: defaultConfig.lpRewardsContract || zeroAddress,
44 | mobilePassportDomain: chain === 'mainnet' ? 'https://passports.nation3.org' : `https://mobile-passport-${chain}.vercel.app`,
45 | nationPassportNFT: defaultConfig.nationPassportNFT || zeroAddress,
46 | nationPassportNFTIssuer: defaultConfig.nationPassportNFTIssuer || zeroAddress,
47 | nationPassportAgreementStatement: defaultConfig.nationPassportAgreementStatement || "",
48 | nationPassportAgreementURI: defaultConfig.nationPassportAgreementURI || "",
49 | }
50 |
51 | console.log(config)
52 |
53 | export const {
54 | nationToken,
55 | veNationToken,
56 | balancerDomain,
57 | balancerVault,
58 | balancerPoolId,
59 | balancerLPToken,
60 | etherscanDomain,
61 | lpRewardsContract,
62 | mobilePassportDomain,
63 | nationPassportNFT,
64 | nationPassportNFTIssuer,
65 | nationPassportAgreementStatement,
66 | nationPassportAgreementURI,
67 | } = config
68 |
69 |
--------------------------------------------------------------------------------
/ui/lib/ve-token.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import VotingEscrow from '../abis/VotingEscrow.json'
3 | import { veNationToken } from '../lib/config'
4 | import { useBalance, useContractRead, useContractWrite } from './use-wagmi'
5 |
6 | const contractParams = {
7 | address: veNationToken,
8 | abi: VotingEscrow.abi,
9 | }
10 |
11 | export function useVeNationBalance(address: any) {
12 | return useBalance({
13 | address,
14 | token: veNationToken,
15 | watch: true,
16 | enabled: !!address,
17 | })
18 | }
19 |
20 | let gasLimits = {
21 | locked: 330000,
22 | create_lock: 1000000,
23 | increase_amount: 1000000,
24 | increase_unlock_time: 1000000,
25 | withdraw: 800000,
26 | }
27 |
28 | export function useVeNationLock(address: any) {
29 | return useContractRead(
30 | {
31 | ...contractParams,
32 | watch: true,
33 | enabled: !!address,
34 | },
35 | 'locked',
36 | [address]
37 | )
38 | }
39 |
40 | export function useVeNationCreateLock(amount: any, time: any) {
41 | return useContractWrite(
42 | contractParams,
43 | 'create_lock',
44 | [amount, time],
45 | { gasLimit: gasLimits.create_lock },
46 | )
47 | }
48 |
49 | export function useVeNationIncreaseLock({
50 | newAmount,
51 | currentTime,
52 | newTime,
53 | }: any) {
54 | const { writeAsync: increaseLockAmount, data: lockAmountData } =
55 | useVeNationIncreaseLockAmount(newAmount)
56 | const { writeAsync: increaseLockTime, data: lockTimeData } =
57 | useVeNationIncreaseLockTime(newTime)
58 | return useMemo(() => {
59 | if (newAmount && newAmount.gt(0)) {
60 | return { writeAsync: increaseLockAmount, data: lockAmountData }
61 | }
62 | if (newTime && currentTime && newTime.gt(currentTime)) {
63 | return { writeAsync: increaseLockTime, data: lockTimeData }
64 | }
65 | return {}
66 | }, [newAmount, currentTime, newTime, increaseLockAmount, increaseLockTime, lockAmountData, lockTimeData])
67 | }
68 |
69 | export function useVeNationIncreaseLockAmount(amount: any) {
70 | return useContractWrite(
71 | contractParams,
72 | 'increase_amount',
73 | [amount],
74 | { gasLimit: gasLimits.increase_amount }
75 | )
76 | }
77 |
78 | export function useVeNationIncreaseLockTime(time: any) {
79 | return useContractWrite(
80 | contractParams,
81 | 'increase_unlock_time',
82 | [time],
83 | { gasLimit: gasLimits.increase_unlock_time }
84 | )
85 | }
86 |
87 | export function useVeNationWithdrawLock() {
88 | return useContractWrite(
89 | contractParams,
90 | 'withdraw',
91 | { gasLimit: gasLimits.withdraw }
92 | )
93 | }
94 |
95 | export function useVeNationSupply() {
96 | return useContractRead(contractParams, 'totalSupply')
97 | }
98 |
--------------------------------------------------------------------------------
/ui/pages/liquidity.tsx:
--------------------------------------------------------------------------------
1 | import { InformationCircleIcon } from '@heroicons/react/24/outline'
2 | import React from 'react'
3 | import { useBalancerPool } from '../lib/balancer'
4 | import { balancerPoolId } from '../lib/config'
5 | import {
6 | useLiquidityRewards,
7 | useWithdrawAndClaim,
8 | } from '../lib/liquidity-rewards'
9 | import { useAccount } from '../lib/use-wagmi'
10 | import ActionButton from '../components/ActionButton'
11 | import Balance from '../components/Balance'
12 | import Head from '../components/Head'
13 | import MainCard from '../components/MainCard'
14 |
15 | export default function Liquidity() {
16 | const { address } = useAccount()
17 |
18 | const {
19 | poolValue,
20 | nationPrice,
21 | isLoading: poolLoading,
22 | } = useBalancerPool(balancerPoolId)
23 |
24 | const {
25 | unclaimedRewards,
26 | userDeposit,
27 | loading: liquidityRewardsLoading,
28 | } = useLiquidityRewards({
29 | nationPrice,
30 | poolValue,
31 | address,
32 | })
33 |
34 | const withdrawAndClaimRewards = useWithdrawAndClaim()
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 | Our liquidity rewards program is over.
42 |
43 | Please{' '}
44 |
45 | withdraw your LP tokens and rewards
46 | {' '}
47 | as soon as possible.
48 |
49 |
50 |
51 |
Your stake
52 |
53 |
54 |
55 |
56 |
57 |
LP tokens
58 |
59 |
60 |
Your rewards
61 |
62 |
63 |
64 |
65 |
66 |
NATION tokens
67 |
68 |
69 |
73 | Withdraw all and claim
74 |
78 |
79 |
80 |
81 |
82 | >
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/ui/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | UserPlusIcon,
3 | LockClosedIcon,
4 | PlusIcon,
5 | } from '@heroicons/react/24/outline'
6 | import Image from 'next/image'
7 | import Link from 'next/link'
8 | import React from 'react'
9 | import { balancerDomain, nationToken } from '../lib/config'
10 | import GradientLink from '../components/GradientLink'
11 | import Head from '../components/Head'
12 | import HomeCard from '../components/HomeCard'
13 | import flag from '../public/flag.svg'
14 |
15 | export default function Index() {
16 | return (
17 | <>
18 |