diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..768b3f9e5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "next/babel", + "@babel/preset-typescript", + ["@babel/preset-react", { "runtime": "automatic" }] + ], + "plugins": ["macros"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..710ab9422 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +build +node_modules +.eslintrc.json +.vscode +public/vendor +src/generated +src/components/v2v3/V2V3Project/V2V3ProjectSettings/pages/ProjectNftSettingsPage/Capsules.module.css diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..1424c3dab --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,72 @@ +{ + "env": { + "node": true, + "browser": true, + "es2021": true + }, + "settings": { + "react": { + "version": "detect" + } + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "overrides": [ + { + "files": ["*.config.js", "*.setup.ts"], + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + } + ], + "plugins": ["react", "@typescript-eslint", "import", "react-hooks"], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off", + "react/prop-types": "off", + "react/no-unescaped-entities": "off", + "react/react-in-jsx-scope": "off", + + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + + "@typescript-eslint/no-explicit-any": "error", + "react-hooks/exhaustive-deps": "error", + "@typescript-eslint/no-redeclare": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "typeLike", + "format": ["PascalCase", "UPPER_CASE"] + } + ], + "no-console": [ + "error", + { "allow": ["warn", "error", "info", "time", "timeEnd"] } + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "lodash", + "message": "Import from 'lodash/' directly to support tree-shaking." + } + ] + } + ], + "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }] + } +} diff --git a/.example.env b/.example.env index ef2da5fa6..345be632d 100644 --- a/.example.env +++ b/.example.env @@ -1,7 +1,72 @@ -REACT_APP_INFURA_ID= -REACT_APP_SUBGRAPH_URL= -REACT_APP_INFURA_NETWORK= # e.g. mainnet -REACT_APP_PINATA_PINNER_KEY= -REACT_APP_PINATA_PINNER_SECRET= -REACT_APP_BLOCKNATIVE_API_KEY= -REACT_APP_SUBGRAPH_URL= \ No newline at end of file +NEXT_PUBLIC_INFURA_ID= + +# Name of Infura network (e.g. 'mainnet', 'goerli') +NEXT_PUBLIC_INFURA_NETWORK= + +# Query URL for Juicebox subgraph used by frontend +NEXT_PUBLIC_SUBGRAPH_URL= + +# Query URL for Juicebox subgraph used by server +NEXT_SUBGRAPH_URL= + +# Query URL for Juicebox subgraph used to load schema for graphql generation +GRAPHQL_SCHEMA_SUBGRAPH_URL= + +# Base URL of site - for dev, just use http://localhost:3000/ +NEXT_PUBLIC_BASE_URL= + +# sentry url. Not needed for development +NEXT_PUBLIC_SENTRY_DSN= + +# e.g. aeolian-local.infura-ipfs.io +NEXT_PUBLIC_INFURA_IPFS_HOSTNAME= + +# prod-only +NEXT_PUBLIC_ARCX_API_KEY= + +# Postmark Email Server Token +POSTMARK_SERVER_TOKEN= + +# Bearer token used to validate front end requests from authorized Juicebox requests. +JUICE_API_BEARER_TOKEN= +# Flag required to enable juice api events. +JUICE_API_EVENTS_ENABLED= + +# Discord Bot Token - used for /about page data sync +DISCORD_BOT_TOKEN= +JUICEBOX_GUILD_ID="775859454780244028" + +# Supabase +SUPABASE_PROJECT_ID= +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= +SUPABASE_JWT_SECRET= + + +# development-only +NEXT_PUBLIC_TENDERLY_API_KEY= +NEXT_PUBLIC_TENDERLY_PROJECT_NAME= +NEXT_PUBLIC_TENDERLY_ACCOUNT= + +### Serverside Variables ### + +# Additional Infura project id used for nextjs page rendering. This is needed +# due to the whitelisting in prod of the main infura id +PRE_RENDER_INFURA_ID= + +INFURA_IPFS_PROJECT_ID= +INFURA_IPFS_API_SECRET= + +# Discord webhook URL for logging updates to projects table in database +DB_PROJECTS_WEBHOOK_URL= + +# Discord Webhook URL for /contact page. +CONTACT_WEBHOOK_URL= + +# Github access token (for project archival requests) +GITHUB_ACCESS_TOKEN= + +# wallet connect +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a1ffbd047 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/styles/antd.* linguist-vendored diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..ce3550040 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://juicebox.money/@juicebox diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f216457c5..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Bug report -about: I can't use the app because something is broken or too confusing -title: "[BUG] " -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Setup (please complete the following information):** - - Device/OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Hardware wallet [e.g. Ledger w/ Metamask] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index a850f7d96..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[IDEA] " -labels: idea -assignees: '' - ---- - -**Is your feature request related to a problem?** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the feature/solution you'd like** -A clear and concise description of what you want to happen. - -**More details** -Add any other notes or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/improvement.md b/.github/ISSUE_TEMPLATE/improvement.md deleted file mode 100644 index ea7a1d44f..000000000 --- a/.github/ISSUE_TEMPLATE/improvement.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Improvement -about: Not quite a bug, but something could be better -title: "[BUGish]" -labels: bugish -assignees: '' - ---- - -**Describe the issue** -A clear and concise description of what could be better. - -**Proposed solution** -A description of the change you'd like to see. - -**Screenshots** -If applicable, add screenshots to help explain your problem. diff --git a/.github/PULL_REQUEST_TEMPLATE/default.md b/.github/PULL_REQUEST_TEMPLATE/default.md new file mode 100644 index 000000000..12f48f320 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/default.md @@ -0,0 +1,13 @@ +## What does this PR do and why? + +_Provide a description of what this PR does. Link to any relevant GitHub issues, Notion tasks or Discord discussions._ + +## Screenshots or screen recordings + +_If applicable, provide screenshots or screen recordings to demonstrate the changes._ + +## Acceptance checklist + +- [ ] I have evaluated the [Approval Guidelines](https://github.com/jbx-protocol/juice-interface/blob/main/CONTRIBUTING.md#approval-guidelines) for this PR. +- [ ] (if relevant) I have tested this PR in [all supported browsers](https://github.com/jbx-protocol/juice-interface/blob/main/CONTRIBUTING.md#supported-browsers). +- [ ] (if relevant) I have tested this PR in dark mode and light mode (if applicable). diff --git a/.github/PULL_REQUEST_TEMPLATE/subgraph.md b/.github/PULL_REQUEST_TEMPLATE/subgraph.md new file mode 100644 index 000000000..dd5f16673 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/subgraph.md @@ -0,0 +1,25 @@ +## What does this PR do and why? + +## Testing instructions: + +New subgraph URL: + +## Subgraph QA checklist + +- [x] V1 project + - [x] page renders + - [x] shows volume + - [x] shows activity feed + - [x] can filter activity feed + - [x] can download activity feed CSV +- [x] V3 Project on JB 3.0 renders +- [x] V3 Project on JB 3.1 renders + - [x] page renders + - [x] shows volume + - [x] shows activity feed + - [x] can filter activity feed + - [x] can download activity feed CSV +- [x] V3 Project **with a handle** renders +- [x] Homepage + - [x] Trending projects renders + - [x] Homepage activity feed renders diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml new file mode 100644 index 000000000..016dbe226 --- /dev/null +++ b/.github/workflows/ci-run.yml @@ -0,0 +1,143 @@ +name: CI + +on: + pull_request: + types: + - ready_for_review + - reopened + - opened + - synchronize + branches: + - main + push: + branches: + - main + + # manual trigger from Github UI - Action tab + workflow_dispatch: + +env: + NEXT_PUBLIC_INFURA_NETWORK: mainnet + PRE_RENDER_INFURA_ID: ${{ secrets.PRE_RENDER_INFURA_ID }} + GRAPHQL_SCHEMA_SUBGRAPH_URL: ${{ secrets.GRAPHQL_SCHEMA_SUBGRAPH_URL }} + +jobs: + jest: + name: Run tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + - name: Install packages + run: yarn install + + - name: Tests with yarn + run: yarn test + + lint-translations: + name: Lint translation source files + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + - name: Install packages + run: yarn install + + - name: Lint translations template + run: 'yarn i18n:lint' + + - uses: tj-actions/changed-files@v24.1 + id: changed-files + with: + files: '**/*.po' + + - name: Check for modified translation source files + if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request' + run: | + echo "🍎 PRs should not change .po files. Only changes to the messages.pot are allowed." + exit 1 + + compile: + name: Compile Typescript + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + - name: Install packages + run: yarn install --frozen-lockfile + + - name: Codegen + run: 'yarn codegen' + + - name: Compile + run: 'yarn ts:compile' + + build-db-types: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: supabase/setup-cli@v1 + with: + version: 1.38.7 + + - name: Start Supabase local development setup + run: supabase start + + - name: Verify generated types are up-to-date + run: | + npm i -g prettier + supabase gen types typescript --local > src/types/database.types.ts + prettier --write src/types/database.types.ts + if [ "$(git diff --ignore-space-at-eol src/types/database.types.ts | wc -l)" -gt "0" ]; then + echo "Detected uncommitted changes after build. See status below:" + git diff + exit 1 + fi + + # next-build: + # name: Build + # runs-on: ubuntu-latest + # needs: compile + + # steps: + # - uses: actions/checkout@v3 + + # - uses: actions/cache@v2 + # with: + # path: '**/node_modules' + # key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + # - name: Install packages + # run: yarn install --frozen-lockfile + + # - name: Build NextJS app + # run: 'yarn build' diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml new file mode 100644 index 000000000..cd4216e86 --- /dev/null +++ b/.github/workflows/crowdin-download.yml @@ -0,0 +1,33 @@ +name: Crowdin download + +on: + schedule: + - cron: '0 0 * * 0' # every Sunday at 00:00 we download translations from Crowdin and make PR to main + + # manual trigger from Github UI - Action tab + workflow_dispatch: + +jobs: + download-translations: + name: Download translations from Crowdin + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Download from Crowdin + uses: crowdin/github-action@1.4.9 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_translations: false + download_translations: true + create_pull_request: true + pull_request_title: 'New Crowdin Translations [skip ci]' + project_id: '492549' + token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + source: 'src/locales/messages.pot' + translation: 'src/locales/%two_letters_code%/messages.po' diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml new file mode 100644 index 000000000..e1e706f2d --- /dev/null +++ b/.github/workflows/crowdin-upload.yml @@ -0,0 +1,38 @@ +name: Crowdin upload + +on: + push: + branches: + - main + + # manual trigger from Github UI - Action tab + workflow_dispatch: + +jobs: + synchronize-with-crowdin: + name: Upload sources to Crowdin + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Extract translations + run: 'yarn i18n:extract' + + - name: Upload to Crowdin + uses: crowdin/github-action@1.4.9 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_translations: true + download_translations: false + project_id: '492549' + token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + source: 'src/locales/messages.pot' + translation: 'src/locales/%two_letters_code%/messages.po' diff --git a/.github/workflows/goerli.yml b/.github/workflows/goerli.yml new file mode 100644 index 000000000..564991e17 --- /dev/null +++ b/.github/workflows/goerli.yml @@ -0,0 +1,25 @@ +name: Deploy Migrations to Goerli + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-22.04 + + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ secrets.GOERLI_DB_PASSWORD }} + GOERLI_PROJECT_ID: ${{ secrets.GOERLI_PROJECT_ID }} + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + + - run: | + supabase link --project-ref $GOERLI_PROJECT_ID + supabase db push diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml new file mode 100644 index 000000000..9133de548 --- /dev/null +++ b/.github/workflows/production.yml @@ -0,0 +1,25 @@ +name: Deploy Migrations to Production + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-22.04 + + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }} + PRODUCTION_PROJECT_ID: ${{ secrets.PRODUCTION_PROJECT_ID }} + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + + - run: | + supabase link --project-ref $PRODUCTION_PROJECT_ID + supabase db push diff --git a/.gitignore b/.gitignore index d64f69f14..d39e3f1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,5 @@ -packages/hardhat/*.txt -**/aws.json - -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -**/node_modules -packages/hardhat/artifacts -packages/app/src/contracts/temp -packages/app/src/contracts/local -packages/app/src/contracts/localhost -packages/app/src/styles/antd.css -packages/hardhat/cache - -docker/**/data - # dependencies /node_modules -/.pnp -.pnp.js # testing coverage @@ -39,3 +23,26 @@ yarn-error.log* .idea mnemonic.txt + +# i18n +src/locales/**/*.js +src/locales/en/messages.po + +# compiled artifacts +src/styles/antd.css + +# Optional REPL history +.node_repl_history +.next + +# GraphQL Codegen +graphql.schema.json +src/generated + +# tsc +*.tsbuildinfo +# Sentry +.sentryclirc + +# Sentry +next.config.original.js diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..d2ae35e84 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..184eab8da --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn i18n:lint \ No newline at end of file diff --git a/.linguirc.json b/.linguirc.json new file mode 100644 index 000000000..ee7f81e80 --- /dev/null +++ b/.linguirc.json @@ -0,0 +1,19 @@ +{ + "catalogs": [ + { + "path": "src/locales/{locale}/messages", + "include": ["src"] + } + ], + "format": "po", + "formatOptions": { + "lineNumbers": false, + "origins": false + }, + "orderBy": "messageId", + "fallbackLocales": { + "default": "en" + }, + "locales": ["en", "zh"], + "sourceLocale": "en" +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..4526ad9e4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +build/ +public/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 27e49247e..201d218f2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,14 @@ "singleQuote": true, "tabWidth": 2, "semi": false, - "trailingComma": "all" + "trailingComma": "all", + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 600 + } + } + ], + "organizeImportsSkipDestructiveCodeActions": true } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c1e657ac1..1d7ac851e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,3 @@ { - "recommendations": [ - "rvest.vs-code-prettier-eslint" - ] -} \ No newline at end of file + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 7ba84c798..75e1b39fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,10 @@ { - "editor.tabSize": 2, -} \ No newline at end of file + "editor.tabSize": 2, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "scss.lint.unknownAtRules": "ignore", + "files.exclude": { + "**/__snapshots__/**": true + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aaf0dcf33..511e8cf7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,42 @@ -# Contributing +# Contributing to Juicebox If you're interested in contributing ideas or code to Juicebox, you're in the right place! -# Development +## Development -Check out the README for instructions on running the app indevelopment. +Read the [development guidelines](doc/development.md) for instructions on running the app in development. -## JB app release + contributions process +## Getting your pull request reviewed, approved, and merged -All changes to the `main` branch will be automatically via [Fleek](https://fleek.co). Contributions to `main` should be created as a pull request, and a live Fleek preview will be automatically deployed. Before merged, each PR must: +Create a pull request (PR) that targets the `main` branch. A live Fleek preview will be automatically deployed for each PR. -- Verify that all CI checks pass before merging -- For significant UX changes, be discussed by other design/dev contributors -- Be approved by at least one code owner +When your PR has met the [approval guidelines](#approval-guidelines) and is ready for review, `@mention` a [codeowner](.github/CODEOWNERS) and ask for a review. -## Finding a first issue +### Internationalization -Start with issues with the label -[`good first issue`](https://github.com/jbx-protocol/juice-juicehouse/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). +If you've added or changed any user-facing strings, you'll need to update the translation source files (externalization). [Learn more](./doc/internationalization.md) about how to externalize strings. -# Ideas +### Git workflow -**Have an idea or suggestion?** Create a new Github [issue](https://github.com/jbx-protocol/juice-juicehouse/issues/new/choose), or mention it in the [Discord](https://discord.gg/6jXrJSyDFf) +`main` is our main branch. -**Notice something broken?** File a [bug report](https://github.com/jbx-protocol/juice-juicehouse/issues/new/choose) +Developers should create branches from the `main` branch. Once the code is ready, create a PR that targets the `main` branch. + +We use the **Squash and Merge** policy for merging PRs. + +### Approval guidelines + +Before your PR is merged, it must meet the following criteria: + +1. The PR follows the [Git workflow](#git-workflow). +1. All CI checks pass. +1. The PR is approved by at least one [codeowner](.github/CODEOWNERS). +1. Significant UI/UX changes are discussed by other design/dev contributors. + +### Supported browsers + +Juicebox supports the following web browsers: + +- Google Chrome +- Mozilla Firefox +- Chromium-based browsers (e.g. Brave Browser) diff --git a/README.md b/README.md index a2febaf67..38e9f69a2 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,44 @@ # juice-interface -Juicebox frontend application. +
+ +

+ The Juicebox frontend application. +

+
-- Mainnet: https://juicebox.money -- Rinkeby: https://rinkeby.juicebox.money +`juice-interface` is a web application for the [Juicebox](https://info.juicebox.money/) protocol. -## Usage +## Links -1. Clone the respository: +| Name | Link | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Mainnet | https://juicebox.money | +| Goerli | https://goerli.juicebox.money | +| Subgraph | [https://thegraph.com/explorer/subgraph...](https://thegraph.com/explorer/subgraph?id=FVmuv3TndQDNd2BWARV8Y27yuKKukryKXPzvAS5E7htC) | - ```bash - git clone https://github.com/jbx-protocol/juice-juicehouse.git - cd juice-juicehouse - ``` +## Report a bug -1. Create an `.env` file in the root directory which mirrors the `.example.env` file. +Mention `@Peel` in [Discord](https://discord.gg/6jXrJSyDFf) and someone from our team will triage your request! -1. Install dependencies and start the app: +## Development - ``` - yarn install - yarn start - ``` +Read the [development guidelines](doc/development.md) for instructions on running the app in development. -## Web3 Providers: +## Contributing -The frontend has two different providers that provide different levels of access to different chains: +Anyone can contribute! [Start here](CONTRIBUTING.md). -- `readProvider`: used to read from contracts on network of injected provider (`.env` file points you at testnet or mainnet) -- `signingProvider`: your personal wallet, connected to via [Blocknative](https://docs.blocknative.com/onboard). +[Learn more](https://www.notion.so/juicebox/Frontend-26b80fcb50b34f3b9356fc7fc5286e05) about the Peel team. +### Support -## Deployment +Join the [JuiceboxDAO Discord](https://discord.gg/6jXrJSyDFf) and reach out to the development team for development support. -Frontend application(s) are deployment automatically on pushes to `main` using [Fleek](https://app.fleek.co/#/sites/juicebox-kovan). +## Team -## Theme +[JuiceboxDAO](https://juicebox.money/@juicebox) funds [Peel](https://juicebox.money/@peel) to build and maintain the application. [Learn more](https://www.notion.so/juicebox/Frontend-26b80fcb50b34f3b9356fc7fc5286e05) about Peel. -The app uses the `SemanticTheme` pattern defined in src/models/semantic-theme, which allows mapping style properties to any number of enumerated `ThemeOption`s. These properties are defined in src/constants/theme. Theme styles can be accessed via `ThemeContext` defined in src/contexts/themeContext and instantiated in src/hooks/JuiceTheme, or via CSS root variables. - -The app also relies on [antd](https://ant-design.gitee.io/) components. To make Antd compatible with `SemanticTheme`, overrides are defined in src/styles/antd-overrides. + + + diff --git a/app.template.yaml b/app.template.yaml deleted file mode 100644 index b7f9c2e77..000000000 --- a/app.template.yaml +++ /dev/null @@ -1,12 +0,0 @@ -runtime: python27 -api_version: 1 -threadsafe: true -default_expiration: '1s' -handlers: - - url: /(.*\.(css|html|ico|js|svg|otf)) - static_files: build/\1 - upload: build/(.*\.(css|html|ico|js|svg|otf)) - - url: /.* - static_files: build/index.html - upload: build/index.html - secure: always \ No newline at end of file diff --git a/app.yaml b/app.yaml deleted file mode 100644 index 48e04d1c6..000000000 --- a/app.yaml +++ /dev/null @@ -1,12 +0,0 @@ -runtime: python27 -api_version: 1 -threadsafe: true -default_expiration: '1s' -handlers: - - url: /(.*\.(css|html|ico|js|svg|otf)) - static_files: build/\1 - upload: build/(.*\.(css|html|ico|js|svg|otf)) - - url: /.* - static_files: build/index.html - upload: build/index.html - secure: always diff --git a/codegen.yml b/codegen.yml new file mode 100644 index 000000000..bd56421ae --- /dev/null +++ b/codegen.yml @@ -0,0 +1,23 @@ +overwrite: true +schema: ${GRAPHQL_SCHEMA_SUBGRAPH_URL} +documents: 'src/graphql/**/*.graphql' +generates: + src/generated/graphql.tsx: + plugins: + - 'typescript' + - 'typescript-operations' + - 'typescript-resolvers' + - 'typescript-react-apollo' + - add: + content: "import { BigNumber } from 'ethers'" + ./graphql.schema.json: + plugins: + - 'introspection' +config: + namingConvention: + enumValues: keep + scalars: + BigInt: BigNumber + avoidOptionals: + field: true + skipTypename: true \ No newline at end of file diff --git a/cypress.json b/cypress.json new file mode 100644 index 000000000..51f462a87 --- /dev/null +++ b/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:3000/", + "chromeWebSecurity": true +} diff --git a/cypress/integration/create.spec.ts b/cypress/integration/create.spec.ts new file mode 100644 index 000000000..1c405a1f6 --- /dev/null +++ b/cypress/integration/create.spec.ts @@ -0,0 +1,50 @@ +import { ProviderSupport } from '../support/provider' + +context('/Create', () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cy.on('window:before:load', (win: any) => { + win.ethereum = ProviderSupport() + }) + + cy.visit('/create') + cy.connectWallet() + }) + + it('creates a project with all default settings successfully', () => { + // Project details + cy.findByRole('textbox', { name: /project name/i, timeout: 10000 }).type( + 'test', + ) + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // Cycles + cy.findByRole('button', { name: /unlocked cycles/i }).click() + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // Payouts + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // Token + cy.findByRole('button', { name: /basic token rules default/i }).click() + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // NFTs + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // Deadline + cy.findByRole('button', { name: /next arrow-right/i }).click() + + // Review + cy.findByRole('checkbox').click() + cy.findByRole('button', { name: /^deploy project.*$/i }).click() + + cy.findByText('Transaction pending...', { timeout: 10000 }).should('exist') + cy.findByText('Congratulations!', { timeout: 10000 }).should('exist') + }) + + it('shows error when project details has no project name', () => { + cy.findByRole('button', { name: /next arrow-right/i }).click() + cy.findByText(/project name is required/i).should('exist') + }) +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 000000000..240908cc2 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,25 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + on('task', { + // Custom tasks go here + }) + + return config +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 000000000..6f5960e35 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,25 @@ +// Imports + +import '@testing-library/cypress/add-commands' + +// Custom commands go here + +Cypress.Commands.add('clickIfExist', (selector, options = {}) => { + const { timeout = 10000 } = options + cy.get('body').then($body => { + if ($body.find(selector).length > 0) { + cy.get(selector, { timeout }).click() + } + }) +}) + +Cypress.Commands.add('connectWallet', () => { + cy.findByRole('button', { name: /connect/i }).click() + cy.get('onboard-v2') + .shadow() + .findByRole('button', { name: /metamask/i }) + .click() + .then(() => { + cy.clickIfExist('#modal-exit-button') + }) +}) diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 000000000..c7e4bf42b --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// TS Support + +declare global { + namespace Cypress { + interface Chainable { + connectWallet() + clickIfExist(selector: string, options?: { timeout?: number }) + } + } +} diff --git a/cypress/support/provider.ts b/cypress/support/provider.ts new file mode 100644 index 000000000..9d6aa1803 --- /dev/null +++ b/cypress/support/provider.ts @@ -0,0 +1,22 @@ +import { FakeData } from '../utils/fake-data' +import { MockProvider } from '../utils/mock-web3-provider' + +const address = '0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D' +const privateKey = + 'de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3' + +export const ProviderSupport = () => { + const provider = new MockProvider({ + address, + privateKey, + networkVersion: 5, + debug: true, + }) + provider.stubMethod('eth_blockNumber', FakeData.Provider.BlockNumber) + provider.stubMethod('eth_sendTransaction', FakeData.Provider.SendTransaction) + provider.stubMethod( + 'eth_getTransactionByHash', + FakeData.Provider.GetTransactionByHash, + ) + return provider +} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000..351f31679 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["cypress", "@testing-library/cypress", "node"] + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/cypress/utils/fake-data/fake-transaction.ts b/cypress/utils/fake-data/fake-transaction.ts new file mode 100644 index 000000000..570a68498 --- /dev/null +++ b/cypress/utils/fake-data/fake-transaction.ts @@ -0,0 +1,27 @@ +// This was pulled from a real tx I did on goerli +export const fakeGetTransactionByHashResponse = { + v: '0x01', + r: '0x5843eb7472b393e79704484050a9a1e56b1a9042b7739b9fbee0fde76c4ff398', + s: '0x54ac69485e2a542f8887e38c29a7c29b440d5289db7d845843b2746161c5dd13', + to: '0x1d260de91233e650f136bf35f8a4ea1f2b68adb6', + gas: '0x61378', + from: '0x337699c1bca440d45be89c1c5af26348f0184cdf', + hash: '0x0cf563b90bbb91db704b49b37fe1beaa3385594d13f1964aac5dae31359935f3', + nonce: '0x117', + input: + '0xb3c52673000000000000000000000000337699c1bca440d45be89c1c5af26348f0184cdf00000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d3c21bcecceda10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa818525455c52061455a87c4fb6f3a5e6f91090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000005c0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e516d5776697a5233334635644b7352504d42627761714c46586463525a4e314652387254684d67743165445853470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000bacb87cf7dbddde2299d92673a938e067a9eb290000000000000000000000000000000000000000000000000000000000000000', + value: '0x0', + accessList: null, + blockHash: null, + blockNumber: null, + transactionIndex: null, + type: '0x2', + gasPrice: '0x5b16a3ec', + maxFeePerGas: '0x5b16a3ec', + maxPriorityFeePerGas: '0x59682f00', + gasLimit: '0x61378', + data: '0xb3c52673000000000000000000000000337699c1bca440d45be89c1c5af26348f0184cdf00000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d3c21bcecceda10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa818525455c52061455a87c4fb6f3a5e6f91090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000005c0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e516d5776697a5233334635644b7352504d42627761714c46586463525a4e314652387254684d67743165445853470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000bacb87cf7dbddde2299d92673a938e067a9eb290000000000000000000000000000000000000000000000000000000000000000', +} + +export const fakeSendTransactionResponse = + '0x0cf563b90bbb91db704b49b37fe1beaa3385594d13f1964aac5dae31359935f3' diff --git a/cypress/utils/fake-data/index.ts b/cypress/utils/fake-data/index.ts new file mode 100644 index 000000000..f8c0b21b9 --- /dev/null +++ b/cypress/utils/fake-data/index.ts @@ -0,0 +1,12 @@ +import { + fakeGetTransactionByHashResponse, + fakeSendTransactionResponse, +} from './fake-transaction' + +export const FakeData = { + Provider: { + GetTransactionByHash: fakeGetTransactionByHashResponse, + SendTransaction: fakeSendTransactionResponse, + BlockNumber: '0x1', + }, +} diff --git a/cypress/utils/mock-web3-provider.ts b/cypress/utils/mock-web3-provider.ts new file mode 100644 index 000000000..15b8dcec4 --- /dev/null +++ b/cypress/utils/mock-web3-provider.ts @@ -0,0 +1,199 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { decrypt, personalSign } from '@metamask/eth-sig-util' + +type ProviderSetup = { + address: string + privateKey: string + networkVersion: number + debug?: boolean + manualConfirmEnable?: boolean +} + +type RequestMethod = + | 'eth_requestAccounts' + | 'eth_accounts' + | 'net_version' + | 'eth_chainId' + | 'personal_sign' + | 'eth_decrypt' + | 'eth_blockNumber' + | 'eth_sendTransaction' + | 'eth_getTransactionByHash' + +interface IMockProvider { + request(args: { method: 'eth_accounts'; params: string[] }): Promise + request(args: { + method: 'eth_requestAccounts' + params: string[] + }): Promise + + request(args: { method: 'net_version' }): Promise + request(args: { method: 'eth_chainId'; params: string[] }): Promise + + request(args: { method: 'personal_sign'; params: string[] }): Promise + request(args: { method: 'eth_decrypt'; params: string[] }): Promise + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request(args: { method: string; params?: any[] }): Promise +} + +// eslint-disable-next-line import/prefer-default-export +export class MockProvider implements IMockProvider { + private setup: ProviderSetup + + public isMetaMask = true + + private acceptEnable?: (value: unknown) => void + + private rejectEnable?: (value: unknown) => void + + constructor(setup: ProviderSetup) { + this.setup = setup + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log = (...args: (any | null)[]) => + // eslint-disable-next-line no-console + this.setup.debug && console.log('🦄', ...args) + + get selectedAddress(): string { + return this.setup.address + } + + get networkVersion(): number { + return this.setup.networkVersion + } + + get chainId(): string { + return `0x${this.setup.networkVersion.toString(16)}` + } + + answerEnable(acceptance: boolean) { + if (acceptance) this.acceptEnable!('Accepted') + else this.rejectEnable!('User rejected') + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request({ method, params }: any): Promise { + this.log(`request[${method}]`) + + if (this.stubs[method]) { + const stub = this.stubs[method] + stub.callCount += 1 + + if (stub.responses.length > 0) { + const response = stub.responses.shift() + return response instanceof Error + ? Promise.reject(response) + : Promise.resolve(response) + } + } + + switch (method) { + case 'eth_requestAccounts': + case 'eth_accounts': + if (this.setup.manualConfirmEnable) { + return new Promise((resolve, reject) => { + this.acceptEnable = resolve + this.rejectEnable = reject + }).then(() => [this.selectedAddress]) + } + return Promise.resolve([this.selectedAddress]) + + case 'net_version': + return Promise.resolve(this.setup.networkVersion) + + case 'eth_chainId': + return Promise.resolve(this.chainId) + + case 'eth_estimateGas': + return Promise.resolve('0x5208') + + case 'personal_sign': { + const privateKey = Buffer.from(this.setup.privateKey, 'hex') + + const signed: string = personalSign({ privateKey, data: params[0] }) + + this.log('signed', signed) + + return Promise.resolve(signed) + } + + case 'eth_sendTransaction': { + return Promise.reject( + new Error('This service can not send transactions.'), + ) + } + + case 'eth_decrypt': { + this.log('eth_decrypt', { method, params }) + + const stripped = params[0].substring(2) + const buff = Buffer.from(stripped, 'hex') + const encryptedData = JSON.parse(buff.toString('utf8')) + + const decrypted: string = decrypt({ + encryptedData, + privateKey: this.setup.privateKey, + }) + + return Promise.resolve(decrypted) + } + + default: + this.log(`requesting missing method ${method}`) + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject( + `The method ${method} is not implemented by the mock provider.`, + ) + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendAsync(props: { method: string }, cb: any) { + switch (props.method) { + case 'eth_accounts': + cb(null, { result: [this.setup.address] }) + break + + case 'net_version': + cb(null, { result: this.setup.networkVersion }) + break + + default: + this.log(`Method '${props.method}' is not supported yet.`) + } + } + + on(props: string) { + this.log('registering event:', props) + } + + removeAllListeners() { + this.log('removeAllListeners', null) + } + + // The following methods allow stubbing responses to requests + private stubs: Record = {} + + stubMethod(method: RequestMethod, response: any) { + if (!this.stubs[method]) { + this.stubs[method] = { callCount: 0, responses: [] } + } + this.stubs[method].responses.push(response) + } + + getCallCount(method: RequestMethod): number { + return this.stubs[method]?.callCount || 0 + } + + getResponse(method: RequestMethod, call = 0): any { + return this.stubs[method]?.responses[call] + } + + setCustomResponse(method: RequestMethod, response: any, times = 1) { + for (let i = 0; i < times; i++) { + this.stubMethod(method, response) + } + } +} diff --git a/doc/architecture/theme.md b/doc/architecture/theme.md new file mode 100644 index 000000000..1d7a01d1d --- /dev/null +++ b/doc/architecture/theme.md @@ -0,0 +1,5 @@ +## Theme + +The app uses the `SemanticTheme` pattern defined in the [`src/models/semantic-theme/`](src/models/semantic-theme) directory. This allows mapping style properties to any number of enumerated `ThemeOption`s. Style properties are defined in the [`src/constants/theme/`](src/constants/theme) directory. Theme styles can be accessed via `ThemeContext` defined in [`src/contexts/themeContext.ts`](src/contexts/themeContext.ts) (and instantiated in [`src/hooks/JuiceTheme.tsx`](src/hooks/JuiceTheme.tsx)), or via CSS root variables. + +The app also relies on [antd](https://ant-design.gitee.io/) React components. We override some Antd styles to make Antd compatible with `SemanticTheme`. These overrides are defined in the [`src/styles/antd-overrides/`](src/styles/antd-overrides) directory. \ No newline at end of file diff --git a/doc/architecture/web3-providers.md b/doc/architecture/web3-providers.md new file mode 100644 index 000000000..8a0dcefe5 --- /dev/null +++ b/doc/architecture/web3-providers.md @@ -0,0 +1,6 @@ +## Web3 Providers + +The frontend has two different providers that provide different levels of access to different chains: + +- `readProvider`: used to read from contracts on network of injected provider (`.env` file points you at testnet or mainnet) +- `signingProvider`: your personal wallet, connected to via [Blocknative](https://docs.blocknative.com/onboard). diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 000000000..e96c1825c --- /dev/null +++ b/doc/development.md @@ -0,0 +1,170 @@ +# Development + +## Quick Links + +- [Internationalization](internationalization.md) +- [DevOps](devops.md) +- [Codebase architecture](architecture/) + +## Installation + +1. Create a [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) of this repository. +1. Clone your fork and navigate to the root directory. +1. Install project dependencies. + + ```bash + yarn install + ``` + +1. Install Docker (https://docs.docker.com/get-docker/). +1. Create a `.env` file in the root directory which mirrors the `.example.env` file. Learn how to define each field in the `.env` file in [Setup](#setup). + +## Setup + +`juicebox-interface` relies on a number of services for development. Create an account for each of the following services: + +- [Infura](https://infura.io) + +The following sections describe how to set up each service for local development. + +### Infura + +Juicebox uses [Infura](https://infura.io) to connect to an Ethereum network (mainnet, or one of the testnets). + +Take the following steps to create an Infura project for local development: + +1. Select **Create New Key** to begin creating a new Infura project. +1. Select the **Web 3 API** option from the **Network** dropdown. +1. Enter a **Name** (for example, `juicebox-local`). +1. Select **Create** to create the project. + +Next, copy the following fields into your `.env` file: + +- **Project ID**. In the `.env` file, copy the **Project ID** into the `NEXT_PUBLIC_INFURA_ID` variable. +- **Endpoint**. This is the Ethereum network that will be used for testing. If you don't know which endpoint to use, select **mainnet**. In the `.env` file, copy the network name (e.g. 'mainnet', not the url) into the `NEXT_PUBLIC_INFURA_NETWORK` variable. + +#### Infura IPFS gateway + +1. Select **Create new key** to begin creating a new Infura project. +1. Select **IPFS** from the **NETWORK** dropdown. +1. Enter a **Name** (for example, `juicebox-ipfs-local`). +1. Select **Create** to create the project. + +Next, copy the following fields into your `.env` file: + +- **PROJECT ID**. In the `.env` file, copy the **Project ID** into the `INFURA_IPFS_PROJECT_ID` variable. +- **API KEY SECRET**. In the `.env` file, copy the **API KEY SECRET** into the `INFURA_IPFS_API_SECRET` variable. +- **DEDICATED GATEWAY SUBDOMAIN**. In the `.env` file, copy the **DEDICATED GATEWAY SUBDOMAIN** into the `NEXT_PUBLIC_INFURA_IPFS_HOSTNAME` variable _without the `https://` prefix_. + +### The Graph + +Juicebox uses [The Graph](https://thegraph.com) to query the Ethereum network using a GraphQL API. + +Take the following steps to set up Juicebox's subgraph for local development: + +1. Join [Peel's discord server](https://discord.gg/akpxJZ5HKR). +2. Inquire about mainnet and Goerli subgraph URLs in the [`#dev` channel](https://discord.com/channels/939317843059679252/939705688563810304). +3. Copy the URL into the `NEXT_PUBLIC_SUBGRAPH_URL` variable of the `.env` file. + +### Supabase + +Juicebox uses [Supabase](https://supabase.com/) to store metadata about the site. + +Take the following steps to setup Juicebox's Subgraph for local development: + +1. Ensure that Docker is installed locally (https://docs.docker.com/get-docker/). +1. Run `yarn supabase:start`. This will need to be run every time during development. +1. Once running, some environment variables will be printed to your CLI. Make sure to add them: + +``` +# This is the endpoint for the supabase service - locally it should be "http://localhost:54321" +NEXT_PUBLIC_SUPABASE_URL= +# This is the anonymous JWT used by non-authorized calls to supabase - generated on start (should persist as the same between runs). +NEXT_PUBLIC_SUPABASE_ANON_KEY= +# This is the main role key. Think of it as a super user key. Is used on the server. This is also generated on start (should persist as the same between runs). +SUPABASE_SERVICE_ROLE_KEY= +# This is the JWT used for signing session JWTs by the server. This is also generated on start (should persist as the same between runs). +SUPABASE_JWT_SECRET= +``` + +Locally, you can ignore `SUPABASE_PROJECT_ID`. + +During local dev without a cron, the update routine endpoint /api/projects/update must be called anytime a database is restarted, or when changes to projects need to be reflected in the database. + +## Usage + +1. Run the app in dev mode + + ```bash + yarn dev + ``` + +2. Build a production build + + ```bash + yarn build + ``` + +3. Run a production build locally + + ```bash + yarn build + yarn start + ``` + +## Transaction simulation + +In development, you can simulate transactions using [Tenderly](https://tenderly.co/). Tenderly produces a stacktrace that you can use to debug failing transactions. + +Set up Tenderly for your development environment using the following steps: + +1. Create a Tenderly account +2. Set the following variables in your `.env` file (**without the comments**): + + ``` + # .env + NEXT_PUBLIC_TENDERLY_API_KEY= # your user tenderly api key + NEXT_PUBLIC_TENDERLY_PROJECT_NAME= # your tenderly project + NEXT_PUBLIC_TENDERLY_ACCOUNT= # your user account name + ``` + +3. Start your development server. + + ```bash + yarn dev + ``` + +Once set up, every transaction that you submit will be simulated using Tenderly. + +When a simulation fails, an error is logged to the development console. This log contains a link to the simulation in Tenderly. + +> Note: there is a 50 simulation per month limit per account. + +# Testing + +## Unit tests + +Run the jest test suite using the following command: + +```bash +yarn test +``` + +## End-to-end tests + +### Running Cypress + +1. Start the app in a separate terminal: + + ```bash + yarn build && yarn start + # Alternatively, you can use `yarn dev` for testing + ``` + +1. Open Cypress. + + ```bash + yarn cy:open + ``` + +1. Run tests. diff --git a/doc/devops.md b/doc/devops.md new file mode 100644 index 000000000..430ca49c4 --- /dev/null +++ b/doc/devops.md @@ -0,0 +1,48 @@ +# DevOps + +We use Vercel as our hosting provider. + +Pushing to the `main` branch trigger application deployments on Vercel. + +## TheGraph + +juice-interface relies on a [subgraph on TheGraph](https://github.com/jbx-protocol/juice-subgraph). + +## Supabase + +juice-interface uses a [Supabase deployment](https://supabase.com/) for storing and retrieving metadata on the site. + +### New Subgraph version checklist + +The subgraph has a URL that we use to query it. This URL is associated with a particular version (a.k.a deployment) of the subgraph. TheGraph produces a new URL when a new version of the subgraph is deployed. + +The source of truth for subgraph URLs and versions is TheGraph website. Authenticate as the Peel multisig to access them. [Learn more](https://juicebox.notion.site/Subgraph-Guide-9a19eecda4e3457ab6f6c6ebcad0eaa6). + +The Juicebox Docs page should also be maintained with the latest URLs (although this is a manual process): https://info.juicebox.money/dev/subgraph + +We need to update the following configurations for new subgraph versions: + +#### Vercel + +- [ ] Update **juice-interface-goerli** project + - [ ] `GRAPHQL_SCHEMA_SUBGRAPH_URL` + - [ ] `NEXT_PUBLIC_SUBGRAPH_URL` +- [ ] Update **juice-interface** + - [ ] `GRAPHQL_SCHEMA_SUBGRAPH_URL` + - [ ] `NEXT_PUBLIC_SUBGRAPH_URL` + +#### GitHub + +- [ ] `GRAPHQL_SCHEMA_SUBGRAPH_URL` + 1. Select **Secrets** > **Actions**. + 2. Locate the **Repository secrets** section. + 3. Edit the secret variable + +#### info.juicebox.money website + +- [ ] Update the [Subgraph URL table](https://info.juicebox.money/dev/subgraph/). + - https://github.com/jbx-protocol/juice-docs/blob/main/docs/dev/subgraph/README.md + +#### Announcement + +- [ ] Ping `@dev` role on Discord to notify Peel devs of the status quo. diff --git a/doc/internationalization.md b/doc/internationalization.md new file mode 100644 index 000000000..cc724c197 --- /dev/null +++ b/doc/internationalization.md @@ -0,0 +1,78 @@ +## Internationalization + +Juicebox uses [Crowdin](https://crowdin.com/project/juicebox-interface) for managing translations. A GitHub workflow uploads new strings for translation to the Crowdin project whenever code using the lingui translation macros is merged into `main`. + +Every day, translations are synced back down from Crowdin to a pull request to `main`. We then merge these PR's into `main` manually. + +### Marking strings for translation + +Any user-facing strings that are added or modified in the source code should be marked for translation. Use the `t` macro or the `Trans` component from the `@lingui/macro` library. [Learn more](https://lingui.js.org/ref/macro.html). + +```js +const myString = t`Example text` +``` + +```html +Example text +``` + +**You must extract strings in PRs**. If your PR adds or modifies translated strings, run the following command to generate new `.po` files: + +```bash +yarn i18n:extract +``` + +### Contributing translations + +For details of how to contribute as a translator, see our [How to become a Juicebox translator](https://www.notion.so/juicebox/How-to-become-a-Juicebox-translator-81fdd9344ef043909a48bd7373ef73d7) Notion page. + +### Adding a language (for devs) + +1. Add the locale code, english name, and short and long alias's to `src/constants/locale.ts`. + +```diff +export const SUPPORTED_LANGUAGES: Language = { + en: { code: 'en', name: 'english', short: 'EN', long: 'English' }, + zh: { code: 'zh', name: 'chinese', short: '中文', long: '中文' }, ++ ru: { code: 'ru', name: 'russian', short: 'RU', long: 'Pусский' }, ++ es: { code: 'es', name: 'spanish', short: 'ES', long: 'Español' }, +} +``` + +2. Add the locale code to `SUPPORTED_LOCALES` in `./src/constants/locale.ts` + +```diff +- export const SUPPORTED_LOCALES = ['en', 'zh'] ++ export const SUPPORTED_LOCALES = ['en', 'zh', 'ru', 'es'] +``` + +3. Import the locale plurals in `src/contexts/Language/LanguageProvider.tsx`. + + ```diff + - import { en, zh } from 'make-plural/plurals' + + import { en, zh, ru, es } from 'make-plural/plurals' + ``` + +4. Load the locale plurals in `src/contexts/Language/LanguageProvider.tsx`. + + ```diff + i18n.loadLocaleData({ + en: { plurals: en }, + zh: { plurals: zh }, + + ru: { plurals: ru }, + + es: { plurals: es }, + }) + ``` + +5. Add the locale code to `.linguirc.json`. + + ```diff + - "locales": ["en", "zh"] + + "locales": ["en", "zh", "ru", "es"] + ``` + +6. Extract and compile the strings marked for translation. This creates a directory for the locale within the `./locale/` directory: + + ```bash + yarn i18n:extract && yarn i18n:compile + ``` diff --git a/doc/nft-rewards.md b/doc/nft-rewards.md new file mode 100644 index 000000000..c353262dd --- /dev/null +++ b/doc/nft-rewards.md @@ -0,0 +1,5 @@ +# NFT Rewards + +## Version notes + +- No change between 3.2 and 3.3 (besides contact addresses, of course). 3.3 should use all 3.2 code. diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..9c36d2faf --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + rootDir: './src/', + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + moduleDirectories: ['node_modules', 'src'], + verbose: true, + setupFilesAfterEnv: ['../jest.setup.ts'], +} diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 000000000..13a367bb3 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,16 @@ +require('dotenv').config() +require('@testing-library/jest-dom') +const { i18n } = require('@lingui/core') +const { messages } = require('./src/locales/en/messages.js') +i18n.load('en', messages) +i18n.activate('en') + +jest.clearAllMocks() + +jest.mock('hooks/Wallet/useInitWallet', () => ({ + initWeb3Onboard: jest.fn(), + useInitWallet: jest.fn(), +})) +jest.mock('hooks/Wallet/useWallet', () => ({ + useWallet: jest.fn(), +})) diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 000000000..f225e3685 --- /dev/null +++ b/next.config.js @@ -0,0 +1,172 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs') +const withBundleAnalyzer = require('@next/bundle-analyzer') + +const webpack = require('webpack') + +const WALLET_CONNECT_URLS = [ + 'https://*.walletconnect.com', + 'https://*.walletconnect.org', + 'wss://*.walletconnect.org', + 'wss://*.walletconnect.com', +] + +const INFURA_IPFS_URLS = [ + 'https://*.infura-ipfs.io', + 'https://ipfs.infura.io:5001', +] + +const SCRIPT_SRC = [ + 'https://*.juicebox.money', + 'https://static.hotjar.com', + 'https://script.hotjar.com', + 'https://cdn.usefathom.com', + // Not working as unsafe-eval is required for metamask + // `'sha256-kZ9E6/oLrki51Yx03/BugStfFrPlm8hjaFbaokympXo='`, // hotjar + `'unsafe-eval'`, // hotjar + `'unsafe-inline'`, // MetaMask +] + +const STYLE_SRC = [ + `'unsafe-inline'`, // NextJS, hotjar +] + +const IMG_SRC = [ + 'https://*.juicebox.money', + 'https://juicebox.money', + ...INFURA_IPFS_URLS, + 'https://jbx.mypinata.cloud', + 'https://gateway.pinata.cloud', + 'https://cdn.stamp.fyi', + 'https://ipfs.io', + 'https://cdn.discordapp.com', + '*.walletconnect.com', +] + +const CONNECT_SRC = [ + 'https://api.usekeyp.com', + 'https://subgraph.satsuma-prod.com', + 'https://*.juicebox.money', + 'https://juicebox.money', + 'https://*.infura.io', + ...INFURA_IPFS_URLS, + 'https://api.pinata.cloud', + 'https://jbx.mypinata.cloud', + 'https://api.studio.thegraph.com', + 'https://gateway.thegraph.com', + 'https://api.arcx.money', + 'https://api.tenderly.co', + 'https://*.hotjar.com', + 'https://*.hotjar.io', + 'wss://*.hotjar.com', + 'https://*.safe.global', + 'https://*.snapshot.org', + 'https://*.wallet.coinbase.com', + ...WALLET_CONNECT_URLS, + 'https://*.supabase.co', + 'https://api.ensideas.com', + 'https://*.sentry.io', +] + +const FRAME_ANCESTORS = ['https://*.gnosis.io', 'https://*.safe.global'] + +if (process.env.NODE_ENV === 'development') { + CONNECT_SRC.push('localhost:*') +} + +const FRAME_SRC = ['https://verify.walletconnect.com/'] + +const ContentSecurityPolicy = ` + default-src 'none'; + script-src 'self' ${SCRIPT_SRC.join(' ')}; + style-src 'self' ${STYLE_SRC.join(' ')}; + font-src 'self' data:; + img-src 'self' ${IMG_SRC.join(' ')} data:; + connect-src 'self' ${CONNECT_SRC.join(' ')}; + manifest-src 'self'; + frame-src ${FRAME_SRC.join(' ')}; + media-src 'self' https://jbx.mypinata.cloud ${INFURA_IPFS_URLS.join(' ')}; + frame-ancestors ${FRAME_ANCESTORS.join(' ')}; + form-action 'self'; +` + +const SECURITY_HEADERS = [ + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, // NOTE: gnosis safe is still allowed due to frame-ancestors definition +] + +const nextConfig = { + staticPageGenerationTimeout: 90, + webpack: config => { + config.resolve.fallback = { fs: false, module: false } + // Adds __DEV__ to the build to fix bug in apollo client `__DEV__ is not defined`. + config.plugins.push( + new webpack.DefinePlugin({ + __DEV__: process.env.NODE_ENV !== 'production', + }), + ) + + return config + }, + async redirects() { + return [ + { + source: '/p', + destination: '/projects', + permanent: true, + }, + { + source: '/v2/create', + destination: '/create', + permanent: true, + }, + ] + }, + async rewrites() { + return [ + { + source: '/@:projectId', + destination: '/v2/p/:projectId', + }, + ] + }, + async headers() { + return [ + { + // Apply these headers to all routes in your application. + source: '/:path*', + headers: [ + { + key: 'Content-Security-Policy', + value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(), + }, + ...SECURITY_HEADERS, + ], + }, + ] + }, + pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'], + sentry: { + hideSourceMaps: true, + }, +} + +module.exports = withBundleAnalyzer({ + enabled: process.env.ANALYZE === 'true', + env: { + NEXTAUTH_URL: process.env.NEXTAUTH_URL, + }, +})(withSentryConfig(nextConfig, { silent: true })) diff --git a/package.json b/package.json index 58a0ef889..b1867321c 100644 --- a/package.json +++ b/package.json @@ -1,73 +1,209 @@ { - "name": "@jbox/app", + "name": "juice-interface", "version": "0.1.0", "private": true, - "dependencies": { - "@jbx-protocol/contracts": "^0.0.2", - "@reduxjs/toolkit": "^1.5.0", - "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.1.0", - "@testing-library/user-event": "^12.1.10", - "@types/react-redux": "^7.1.16", - "@uniswap/sdk": "^3.0.3", - "@walletconnect/web3-provider": "^1.5.4", - "antd": "^4.16.7", - "autolinker": "^3.14.3", - "axios": "^0.21.1", - "bnc-notify": "^1.9.1", - "bnc-onboard": "1.34.1", - "env-cmd": "^10.1.0", - "erc-20-abi": "^1.0.0", - "ethereum-block-by-date": "^1.4.2", - "ethers": "^5.1.0", - "react": "^17", - "react-dom": "^17.0.1", - "react-redux": "^7.2.2", - "react-router-dom": "^5.2.0", - "react-scripts": "4.0.1", - "recharts": "^2.1.2", - "sass": "^1.38.1", - "typescript": "^4.0.3", - "use-deep-compare-effect": "^1.6.1", - "web-vitals": "^0.2.4" - }, - "resolutions": { - "jest": "26.6.0", - "**/@typescript-eslint/eslint-plugin": "^4.1.1", - "**/@typescript-eslint/parser": "^4.1.1" + "browser": { + "fs": false }, "scripts": { - "compile-styles": "lessc ./src/styles/antd.less ./src/styles/antd.css --js", - "start": "yarn compile-styles && react-scripts start", - "build": "yarn compile-styles && react-scripts build", - "test": "yarn compile-styles && react-scripts test", - "eject": "react-scripts eject" + "prebuild": "yarn predev", + "build": "NEXT_PUBLIC_VERSION=$(git rev-parse --short HEAD) next build", + "analyze": "ANALYZE=true yarn build", + "codegen": "graphql-code-generator --config codegen.yml -r dotenv/config && node ./scripts/graphql-codegen-override-pv.js", + "css:compile": "lessc ./node_modules/antd/dist/antd.less ./src/styles/antd.css --js", + "predev": "yarn css:compile && yarn i18n:compile && yarn codegen", + "dev": "next dev", + "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", + "i18n:compile": "NODE_ENV=development yarn i18n:extract-source-locale && lingui compile", + "i18n:extract": "./scripts/extract-translations-template.sh", + "i18n:extract-source-locale": "NODE_ENV=development lingui extract --locale en", + "i18n:lint": "./scripts/lint-translations-template.sh", + "postinstall": "yarn i18n:compile", + "lint": "eslint --ext .js,.jsx,.ts,.tsx ./", + "lint-staged": "lint-staged", + "lint:fix": "yarn lint --fix", + "prepare": "husky install", + "pristine": "rm -Rf node_modules && yarn cache clean --all && yarn install", + "start": "NEXT_PUBLIC_VERSION=$(git rev-parse --short HEAD) next start", + "test": "jest", + "ts:compile": "tsc --noEmit --incremental", + "ts:prune": "ts-prune", + "supabase:start": "supabase start", + "supabase:stop": "supabase stop", + "supabase:restart": "yarn supabase:stop && yarn supabase:start", + "supabase:login": "supabase login", + "supabase:pushdb": "supabase db push", + "supabase:diffdb": "supabase db diff", + "supabase:generate:local": "supabase gen types typescript --local > src/types/database.types.ts", + "cy:run": "cypress run", + "cy:open": "CYPRESS_REMOTE_DEBUGGING_PORT=9222 cypress open" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" + "lint-staged": { + "*.{js,jsx,ts,tsx,json,css}": [ + "eslint", + "prettier --write" ] }, "browserslist": { "production": [ - ">0.2%", - "not dead", - "not op_mini all" + "last 1 chrome version", + "last 1 firefox version" ], "development": [ "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" + "last 1 firefox version" ] }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@apollo/client": "3.6.9", + "@arcxmoney/analytics": "1.6.1", + "@graphql-codegen/add": "^4.0.1", + "@headlessui/react": "^1.7.10", + "@heroicons/react": "^2.0.17", + "@jbx-protocol/contracts-v1": "2.0.0", + "@jbx-protocol/contracts-v2-4.0.0": "npm:@jbx-protocol/contracts-v2@4.0.0", + "@jbx-protocol/contracts-v2-latest": "npm:@jbx-protocol/contracts-v2@8.0.4", + "@jbx-protocol/juice-721-delegate-v3": "npm:@jbx-protocol/juice-721-delegate@3.0.0", + "@jbx-protocol/juice-721-delegate-v3-1": "npm:@jbx-protocol/juice-721-delegate@5.0.2", + "@jbx-protocol/juice-721-delegate-v3-2": "npm:@jbx-protocol/juice-721-delegate@6.0.3", + "@jbx-protocol/juice-721-delegate-v3-3": "npm:@jbx-protocol/juice-721-delegate@7.0.0", + "@jbx-protocol/juice-contracts-v3": "4.0.0", + "@jbx-protocol/juice-delegates-registry": "1.0.0", + "@jbx-protocol/juice-v1-token-terminal": "1.0.1", + "@jbx-protocol/juice-v3-migration": "1.0.0", + "@jbx-protocol/project-handles": "^2.0.4", + "@lingui/cli": "^4.0.0", + "@lingui/detect-locale": "^4.0.0", + "@lingui/macro": "^4.0.0", + "@lingui/react": "^4.0.0", + "@metamask/providers": "^10.0.0", + "@reduxjs/toolkit": "^1.6.2", + "@sentry/nextjs": "^7.44.1", + "@supabase/auth-helpers-nextjs": "^0.5.4", + "@supabase/auth-helpers-react": "^0.3.1", + "@supabase/supabase-js": "^2.10.0", + "@sushiswap/sdk": "5.0.0-canary.116", + "@uniswap/sdk": "3.0.3", + "@uniswap/sdk-core": "3.0.1", + "@uniswap/v3-sdk": "3.8.2", + "@usekeyp/js-sdk": "^0.1.7", + "@usekeyp/ui-kit": "^0.1.6", + "@walletconnect/web3-provider": "1.8.0", + "@web3-onboard/coinbase": "2.1.1", + "@web3-onboard/core": "2.8.1", + "@web3-onboard/gnosis": "2.1.5", + "@web3-onboard/injected-wallets": "2.2.0", + "@web3-onboard/keystone": "2.2.1", + "@web3-onboard/ledger": "2.2.1", + "@web3-onboard/react": "2.3.1", + "@web3-onboard/trezor": "2.2.1", + "@web3-onboard/walletconnect": "^2.3.9", + "antd": "^4.24.0", + "apollo-link-scalars": "^4.0.2", + "autolinker": "^3.14.3", + "axios": "0.27.2", + "bottleneck": "^2.19.5", + "discord.js": "^14.9.0", + "erc-20-abi": "^1.0.0", + "eslint-config-next": "^13.4.3", + "ethereum-block-by-date": "1.4.6", + "ethers": "^5.7.0", + "forge-run-parser": "^1.0.2", + "formidable": "^2.0.1", + "formik": "^2.2.9", + "graphql": "^16.5.0", + "he": "^1.2.0", + "jsonwebtoken": "^9.0.0", + "less": "4.1.2", + "lodash": "^4.17.21", + "lottie-react": "^2.4.0", + "mjml": "^4.13.0", + "mustache": "^4.2.0", + "next": "^13.4.3", + "next-auth": "^4.22.1", + "object-hash": "^3.0.0", + "pino": "^8.11.0", + "pino-pretty": "^10.0.0", + "postcss": "^8.4.23", + "postmark": "^3.0.15", + "qs": "^6.11.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-query": "^3.33.2", + "react-redux": "^8", + "react-stop-propagation": "^0.2.0", + "recharts": "^2.6.2", + "sass": "^1.52.1", + "sharp": "^0.31.1", + "swiper": "^9.2.3", + "tailwind-merge": "^1.8.0", + "tiny-invariant": "^1.2.0", + "use-deep-compare-effect": "^1.6.1", + "use-resize-observer": "^9.1.0", + "uuid": "^8.3.2", + "yup": "^1.0.2" + }, "devDependencies": { + "@babel/core": "^7.21.8", + "@babel/preset-env": "^7.22.2", + "@babel/preset-react": "^7.22.3", + "@babel/preset-typescript": "^7.21.5", + "@graphql-codegen/cli": "^3.3.0", + "@graphql-codegen/introspection": "^3.0.1", + "@graphql-codegen/typescript": "3.0.3", + "@graphql-codegen/typescript-operations": "^3.0.3", + "@graphql-codegen/typescript-react-apollo": "^3.3.2", + "@graphql-codegen/typescript-resolvers": "^3.2.0", + "@next/bundle-analyzer": "^13.2.4", + "@testing-library/cypress": "^8.0.2", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.4.3", + "@types/ethereum-block-by-date": "1.4.1", + "@types/form-data": "^2.5.0", + "@types/formidable": "^2.0.5", + "@types/he": "^1.2.0", "@types/jest": "^26.0.15", - "@types/node": "^12.0.0", - "@types/react": "^17", - "@types/react-dom": "^17", - "@types/react-router-dom": "^5.1.7", - "eslint-config-react-app": "^6.0.0", - "less": "^4.1.1" + "@types/jsonwebtoken": "^9.0.2", + "@types/lodash": "^4.14.180", + "@types/mjml": "^4.7.0", + "@types/mustache": "^4.2.2", + "@types/node": "^17.0.26", + "@types/object-hash": "^3.0.2", + "@types/qs": "^6.9.7", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/uuid": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^5.35.1", + "@typescript-eslint/parser": "^5.35.1", + "autoprefixer": "10.4.13", + "babel-jest": "^29.5.0", + "cypress": "^9.5.4", + "dotenv": "^16.0.1", + "eslint": "^8.19.0", + "eslint-config-prettier": "^8.5.0", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-react": "^7.31.1", + "eslint-plugin-react-hooks": "^4.6.0", + "husky": "^7.0.0", + "jest": "^28.1.3", + "jest-dom": "^4.0.0", + "jest-environment-jsdom": "^29.5.0", + "lint-staged": "^13.0.3", + "postcss-cli": "10.0.0", + "prettier": "^2.7.1", + "prettier-plugin-organize-imports": "^3.1.0", + "prettier-plugin-tailwindcss": "^0.1.13", + "react-test-renderer": "^18.2.0", + "supabase": "1.38.7", + "tailwindcss": "3.2.3", + "ts-prune": "^0.10.3", + "typescript": "4.7.4", + "yargs": "^17.6.2" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..33ad091d2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 000000000..81da8b837 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-384x384.png b/public/android-chrome-384x384.png new file mode 100644 index 000000000..425b1f14d Binary files /dev/null and b/public/android-chrome-384x384.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 000000000..59279d109 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/assets/Discord-Logo.svg b/public/assets/Discord-Logo.svg deleted file mode 100644 index 73f126937..000000000 --- a/public/assets/Discord-Logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/assets/JBM-Unfurl-banner.png b/public/assets/JBM-Unfurl-banner.png new file mode 100644 index 000000000..fcc43a4ab Binary files /dev/null and b/public/assets/JBM-Unfurl-banner.png differ diff --git a/public/assets/arrow.png b/public/assets/arrow.png deleted file mode 100644 index ec7ba44aa..000000000 Binary files a/public/assets/arrow.png and /dev/null differ diff --git a/public/assets/banana-cover.png b/public/assets/banana-cover.png deleted file mode 100644 index 8baf8c8bb..000000000 Binary files a/public/assets/banana-cover.png and /dev/null differ diff --git a/public/assets/banana-od.png b/public/assets/banana-od.png deleted file mode 100644 index 2de38802e..000000000 Binary files a/public/assets/banana-od.png and /dev/null differ diff --git a/public/assets/banana-ol.png b/public/assets/banana-ol.png deleted file mode 100644 index 4c9b1a93f..000000000 Binary files a/public/assets/banana-ol.png and /dev/null differ diff --git a/public/assets/banana_dwgj.png b/public/assets/banana_dwgj.png deleted file mode 100644 index 2c9e07e10..000000000 Binary files a/public/assets/banana_dwgj.png and /dev/null differ diff --git a/public/assets/blueberry-ol.png b/public/assets/blueberry-ol.png deleted file mode 100644 index 9ecae1d69..000000000 Binary files a/public/assets/blueberry-ol.png and /dev/null differ diff --git a/public/assets/bolt.png b/public/assets/bolt.png deleted file mode 100644 index a39cdc02f..000000000 Binary files a/public/assets/bolt.png and /dev/null differ diff --git a/public/assets/cooler_if_you_did.png b/public/assets/cooler_if_you_did.png deleted file mode 100644 index ff8675a95..000000000 Binary files a/public/assets/cooler_if_you_did.png and /dev/null differ diff --git a/public/assets/csv/v1-payouts-template.csv b/public/assets/csv/v1-payouts-template.csv new file mode 100644 index 000000000..65e4f7477 --- /dev/null +++ b/public/assets/csv/v1-payouts-template.csv @@ -0,0 +1,2 @@ +beneficiary,percent,preferUnstaked,lockedUntil,projectId,allocator +0x301779a1D31e1b90eb34FAB7e305Fc6e3180F0c2,0.5458,false,0,0,0x0000000000000000000000000000000000000000 diff --git a/public/assets/csv/v1-reserve-template.csv b/public/assets/csv/v1-reserve-template.csv new file mode 100644 index 000000000..dc2875d6d --- /dev/null +++ b/public/assets/csv/v1-reserve-template.csv @@ -0,0 +1,3 @@ +beneficiary,percent,preferUnstaked,lockedUntil +0x0000000000000000000000000000000000000000,0.99,true, +0x0000000000000000000000000000000000000001,0.01,false,1969539909 \ No newline at end of file diff --git a/public/assets/csv/v2-splits-template.csv b/public/assets/csv/v2-splits-template.csv new file mode 100644 index 000000000..67b20eb0f --- /dev/null +++ b/public/assets/csv/v2-splits-template.csv @@ -0,0 +1,3 @@ +beneficiary,percent,preferClaimed,lockedUntil,projectId,allocator +0x0000000000000000000000000000000000000000,0.99,true,1969539909,, +0x0000000000000000000000000000000000000001,0.01,true,1969539909,1, \ No newline at end of file diff --git a/public/assets/empty_orange_dark.png b/public/assets/empty_orange_dark.png new file mode 100644 index 000000000..d284a8f2f Binary files /dev/null and b/public/assets/empty_orange_dark.png differ diff --git a/public/assets/empty_orange_light.png b/public/assets/empty_orange_light.png new file mode 100644 index 000000000..f6abb806c Binary files /dev/null and b/public/assets/empty_orange_light.png differ diff --git a/public/assets/error404.png b/public/assets/error404.png new file mode 100644 index 000000000..751051020 Binary files /dev/null and b/public/assets/error404.png differ diff --git a/public/assets/error500.png b/public/assets/error500.png new file mode 100644 index 000000000..3ee924a94 Binary files /dev/null and b/public/assets/error500.png differ diff --git a/public/assets/fonts/Beatrice-Medium.woff2 b/public/assets/fonts/Beatrice-Medium.woff2 new file mode 100644 index 000000000..8260186d2 Binary files /dev/null and b/public/assets/fonts/Beatrice-Medium.woff2 differ diff --git a/public/assets/fonts/Beatrice-Regular.woff2 b/public/assets/fonts/Beatrice-Regular.woff2 new file mode 100644 index 000000000..a359c6279 Binary files /dev/null and b/public/assets/fonts/Beatrice-Regular.woff2 differ diff --git a/public/assets/fonts/Capsules-500.woff2 b/public/assets/fonts/Capsules-500.woff2 new file mode 100644 index 000000000..e4a2a537f Binary files /dev/null and b/public/assets/fonts/Capsules-500.woff2 differ diff --git a/public/assets/fonts/PPAgrandir-Medium.woff2 b/public/assets/fonts/PPAgrandir-Medium.woff2 new file mode 100644 index 000000000..8b3302868 Binary files /dev/null and b/public/assets/fonts/PPAgrandir-Medium.woff2 differ diff --git a/public/assets/fonts/PPAgrandir-WideBold.woff2 b/public/assets/fonts/PPAgrandir-WideBold.woff2 new file mode 100644 index 000000000..e386c2d29 Binary files /dev/null and b/public/assets/fonts/PPAgrandir-WideBold.woff2 differ diff --git a/public/assets/fonts/PPAgrandir-WideMedium.woff2 b/public/assets/fonts/PPAgrandir-WideMedium.woff2 new file mode 100644 index 000000000..806b9b7ca Binary files /dev/null and b/public/assets/fonts/PPAgrandir-WideMedium.woff2 differ diff --git a/public/assets/fountain_of_juice.png b/public/assets/fountain_of_juice.png deleted file mode 100644 index b3fefab0e..000000000 Binary files a/public/assets/fountain_of_juice.png and /dev/null differ diff --git a/public/assets/glass_overflow.png b/public/assets/glass_overflow.png deleted file mode 100644 index 7735f54c9..000000000 Binary files a/public/assets/glass_overflow.png and /dev/null differ diff --git a/public/assets/images/about/hero.webp b/public/assets/images/about/hero.webp new file mode 100644 index 000000000..15e111aa0 Binary files /dev/null and b/public/assets/images/about/hero.webp differ diff --git a/public/assets/images/about/illustration1.webp b/public/assets/images/about/illustration1.webp new file mode 100644 index 000000000..11b3e3244 Binary files /dev/null and b/public/assets/images/about/illustration1.webp differ diff --git a/public/assets/images/about/illustration2.webp b/public/assets/images/about/illustration2.webp new file mode 100644 index 000000000..a4bf863ab Binary files /dev/null and b/public/assets/images/about/illustration2.webp differ diff --git a/public/assets/images/about/illustration3.webp b/public/assets/images/about/illustration3.webp new file mode 100644 index 000000000..ef9a40ff3 Binary files /dev/null and b/public/assets/images/about/illustration3.webp differ diff --git a/public/assets/images/banny-walk-ol.webp b/public/assets/images/banny-walk-ol.webp new file mode 100644 index 000000000..ab450d14d Binary files /dev/null and b/public/assets/images/banny-walk-ol.webp differ diff --git a/public/assets/images/blueberry-ol.png b/public/assets/images/blueberry-ol.png new file mode 100644 index 000000000..146ae6906 Binary files /dev/null and b/public/assets/images/blueberry-ol.png differ diff --git a/public/assets/images/case-studies/cdao.webp b/public/assets/images/case-studies/cdao.webp new file mode 100644 index 000000000..8b073bfef Binary files /dev/null and b/public/assets/images/case-studies/cdao.webp differ diff --git a/public/assets/images/case-studies/constitution-casestudy-banner.png b/public/assets/images/case-studies/constitution-casestudy-banner.png new file mode 100644 index 000000000..05108e8cd Binary files /dev/null and b/public/assets/images/case-studies/constitution-casestudy-banner.png differ diff --git a/public/assets/images/case-studies/constitution-casestudy-banner2.png b/public/assets/images/case-studies/constitution-casestudy-banner2.png new file mode 100644 index 000000000..d2efeb565 Binary files /dev/null and b/public/assets/images/case-studies/constitution-casestudy-banner2.png differ diff --git a/public/assets/images/case-studies/moondao-casestudy-banner.png b/public/assets/images/case-studies/moondao-casestudy-banner.png new file mode 100644 index 000000000..9acd9ef12 Binary files /dev/null and b/public/assets/images/case-studies/moondao-casestudy-banner.png differ diff --git a/public/assets/images/case-studies/moondao-casestudy-banner2.png b/public/assets/images/case-studies/moondao-casestudy-banner2.png new file mode 100644 index 000000000..9dd237015 Binary files /dev/null and b/public/assets/images/case-studies/moondao-casestudy-banner2.png differ diff --git a/public/assets/images/case-studies/sharkdao-casestudy-banner.png b/public/assets/images/case-studies/sharkdao-casestudy-banner.png new file mode 100644 index 000000000..a55377232 Binary files /dev/null and b/public/assets/images/case-studies/sharkdao-casestudy-banner.png differ diff --git a/public/assets/images/case-studies/sharkdao-casestudy-banner2.png b/public/assets/images/case-studies/sharkdao-casestudy-banner2.png new file mode 100644 index 000000000..496bc3a24 Binary files /dev/null and b/public/assets/images/case-studies/sharkdao-casestudy-banner2.png differ diff --git a/public/assets/images/case-studies/studiodao-casestudy-banner.png b/public/assets/images/case-studies/studiodao-casestudy-banner.png new file mode 100644 index 000000000..8af98591d Binary files /dev/null and b/public/assets/images/case-studies/studiodao-casestudy-banner.png differ diff --git a/public/assets/images/case-studies/studiodao-casestudy-banner2.png b/public/assets/images/case-studies/studiodao-casestudy-banner2.png new file mode 100644 index 000000000..04dab012a Binary files /dev/null and b/public/assets/images/case-studies/studiodao-casestudy-banner2.png differ diff --git a/public/assets/images/contact-hero-od.webp b/public/assets/images/contact-hero-od.webp new file mode 100644 index 000000000..0d3f65c84 Binary files /dev/null and b/public/assets/images/contact-hero-od.webp differ diff --git a/public/assets/images/contact-hero-ol.webp b/public/assets/images/contact-hero-ol.webp new file mode 100644 index 000000000..55b789867 Binary files /dev/null and b/public/assets/images/contact-hero-ol.webp differ diff --git a/public/assets/images/create-success-hero.webp b/public/assets/images/create-success-hero.webp new file mode 100644 index 000000000..6da8113b9 Binary files /dev/null and b/public/assets/images/create-success-hero.webp differ diff --git a/public/assets/images/home/categories/art.jpg b/public/assets/images/home/categories/art.jpg new file mode 100644 index 000000000..5030d6a1d Binary files /dev/null and b/public/assets/images/home/categories/art.jpg differ diff --git a/public/assets/images/home/categories/business.jpg b/public/assets/images/home/categories/business.jpg new file mode 100644 index 000000000..40bd708d7 Binary files /dev/null and b/public/assets/images/home/categories/business.jpg differ diff --git a/public/assets/images/home/categories/charity.jpg b/public/assets/images/home/categories/charity.jpg new file mode 100644 index 000000000..92e74a4f5 Binary files /dev/null and b/public/assets/images/home/categories/charity.jpg differ diff --git a/public/assets/images/home/categories/dao.jpg b/public/assets/images/home/categories/dao.jpg new file mode 100644 index 000000000..3363b0017 Binary files /dev/null and b/public/assets/images/home/categories/dao.jpg differ diff --git a/public/assets/images/home/categories/defi.jpg b/public/assets/images/home/categories/defi.jpg new file mode 100644 index 000000000..b2cbafaea Binary files /dev/null and b/public/assets/images/home/categories/defi.jpg differ diff --git a/public/assets/images/home/categories/education.jpg b/public/assets/images/home/categories/education.jpg new file mode 100644 index 000000000..84a8e790f Binary files /dev/null and b/public/assets/images/home/categories/education.jpg differ diff --git a/public/assets/images/home/categories/events.jpg b/public/assets/images/home/categories/events.jpg new file mode 100644 index 000000000..79600449f Binary files /dev/null and b/public/assets/images/home/categories/events.jpg differ diff --git a/public/assets/images/home/categories/fundraising.jpg b/public/assets/images/home/categories/fundraising.jpg new file mode 100644 index 000000000..6da3594bc Binary files /dev/null and b/public/assets/images/home/categories/fundraising.jpg differ diff --git a/public/assets/images/home/categories/games.jpg b/public/assets/images/home/categories/games.jpg new file mode 100644 index 000000000..65f6ac0a2 Binary files /dev/null and b/public/assets/images/home/categories/games.jpg differ diff --git a/public/assets/images/home/categories/music.jpg b/public/assets/images/home/categories/music.jpg new file mode 100644 index 000000000..6121c562d Binary files /dev/null and b/public/assets/images/home/categories/music.jpg differ diff --git a/public/assets/images/home/categories/nfts.jpg b/public/assets/images/home/categories/nfts.jpg new file mode 100644 index 000000000..3621aff83 Binary files /dev/null and b/public/assets/images/home/categories/nfts.jpg differ diff --git a/public/assets/images/home/categories/social.jpg b/public/assets/images/home/categories/social.jpg new file mode 100644 index 000000000..83c43f692 Binary files /dev/null and b/public/assets/images/home/categories/social.jpg differ diff --git a/public/assets/images/home/categories/software.jpg b/public/assets/images/home/categories/software.jpg new file mode 100644 index 000000000..c9462108b Binary files /dev/null and b/public/assets/images/home/categories/software.jpg differ diff --git a/public/assets/images/home/why-juicebox/blobs-dark/blob-builders.png b/public/assets/images/home/why-juicebox/blobs-dark/blob-builders.png new file mode 100644 index 000000000..3772359fd Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-dark/blob-builders.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-dark/blob-crowdfunding.png b/public/assets/images/home/why-juicebox/blobs-dark/blob-crowdfunding.png new file mode 100644 index 000000000..c6b425a7c Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-dark/blob-crowdfunding.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-dark/blob-daos.png b/public/assets/images/home/why-juicebox/blobs-dark/blob-daos.png new file mode 100644 index 000000000..a0720d033 Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-dark/blob-daos.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-dark/blob-nfts.png b/public/assets/images/home/why-juicebox/blobs-dark/blob-nfts.png new file mode 100644 index 000000000..ee05be582 Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-dark/blob-nfts.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-light/blob-builders.png b/public/assets/images/home/why-juicebox/blobs-light/blob-builders.png new file mode 100644 index 000000000..5cdc3d5ce Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-light/blob-builders.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-light/blob-crowdfunding.png b/public/assets/images/home/why-juicebox/blobs-light/blob-crowdfunding.png new file mode 100644 index 000000000..0c1311dca Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-light/blob-crowdfunding.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-light/blob-daos.png b/public/assets/images/home/why-juicebox/blobs-light/blob-daos.png new file mode 100644 index 000000000..ec5383af9 Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-light/blob-daos.png differ diff --git a/public/assets/images/home/why-juicebox/blobs-light/blob-nfts.png b/public/assets/images/home/why-juicebox/blobs-light/blob-nfts.png new file mode 100644 index 000000000..85eccbd31 Binary files /dev/null and b/public/assets/images/home/why-juicebox/blobs-light/blob-nfts.png differ diff --git a/public/assets/images/home/why-juicebox/builders.webp b/public/assets/images/home/why-juicebox/builders.webp new file mode 100644 index 000000000..e7796f686 Binary files /dev/null and b/public/assets/images/home/why-juicebox/builders.webp differ diff --git a/public/assets/images/home/why-juicebox/crowdfunding.webp b/public/assets/images/home/why-juicebox/crowdfunding.webp new file mode 100644 index 000000000..390ff9d03 Binary files /dev/null and b/public/assets/images/home/why-juicebox/crowdfunding.webp differ diff --git a/public/assets/images/home/why-juicebox/daos.webp b/public/assets/images/home/why-juicebox/daos.webp new file mode 100644 index 000000000..edb110d28 Binary files /dev/null and b/public/assets/images/home/why-juicebox/daos.webp differ diff --git a/public/assets/images/home/why-juicebox/nfts.webp b/public/assets/images/home/why-juicebox/nfts.webp new file mode 100644 index 000000000..daa583681 Binary files /dev/null and b/public/assets/images/home/why-juicebox/nfts.webp differ diff --git a/public/assets/images/juice-homepage-hero.webp b/public/assets/images/juice-homepage-hero.webp new file mode 100644 index 000000000..d9c70d6f9 Binary files /dev/null and b/public/assets/images/juice-homepage-hero.webp differ diff --git a/public/assets/images/juiceboxdao_logo.webp b/public/assets/images/juiceboxdao_logo.webp new file mode 100644 index 000000000..63b457b44 Binary files /dev/null and b/public/assets/images/juiceboxdao_logo.webp differ diff --git a/public/assets/images/quint.webp b/public/assets/images/quint.webp new file mode 100644 index 000000000..9b53e46c7 Binary files /dev/null and b/public/assets/images/quint.webp differ diff --git a/public/assets/images/sassy-blueberry.webp b/public/assets/images/sassy-blueberry.webp new file mode 100644 index 000000000..51f6ee9ce Binary files /dev/null and b/public/assets/images/sassy-blueberry.webp differ diff --git a/public/assets/images/star-wars/may-4th.png b/public/assets/images/star-wars/may-4th.png new file mode 100644 index 000000000..21ffcb2f7 Binary files /dev/null and b/public/assets/images/star-wars/may-4th.png differ diff --git a/public/assets/images/star-wars/yoda.webp b/public/assets/images/star-wars/yoda.webp new file mode 100644 index 000000000..276055eda Binary files /dev/null and b/public/assets/images/star-wars/yoda.webp differ diff --git a/public/assets/images/stickers/banny_blockchain.png b/public/assets/images/stickers/banny_blockchain.png new file mode 100644 index 000000000..2a8d14ab9 Binary files /dev/null and b/public/assets/images/stickers/banny_blockchain.png differ diff --git a/public/assets/images/stickers/banny_coder.png b/public/assets/images/stickers/banny_coder.png new file mode 100644 index 000000000..b9ec669ae Binary files /dev/null and b/public/assets/images/stickers/banny_coder.png differ diff --git a/public/assets/images/stickers/banny_dao.png b/public/assets/images/stickers/banny_dao.png new file mode 100644 index 000000000..dc621151e Binary files /dev/null and b/public/assets/images/stickers/banny_dao.png differ diff --git a/public/assets/images/stickers/banny_funeral.png b/public/assets/images/stickers/banny_funeral.png new file mode 100644 index 000000000..c631132a9 Binary files /dev/null and b/public/assets/images/stickers/banny_funeral.png differ diff --git a/public/assets/images/stickers/banny_lfg.png b/public/assets/images/stickers/banny_lfg.png new file mode 100644 index 000000000..e74d1740b Binary files /dev/null and b/public/assets/images/stickers/banny_lfg.png differ diff --git a/public/assets/images/stickers/banny_love.png b/public/assets/images/stickers/banny_love.png new file mode 100644 index 000000000..7d973beaa Binary files /dev/null and b/public/assets/images/stickers/banny_love.png differ diff --git a/public/assets/images/stickers/banny_party.png b/public/assets/images/stickers/banny_party.png new file mode 100644 index 000000000..d55f1e716 Binary files /dev/null and b/public/assets/images/stickers/banny_party.png differ diff --git a/public/assets/images/stickers/banny_party_2.png b/public/assets/images/stickers/banny_party_2.png new file mode 100644 index 000000000..c3bb19bef Binary files /dev/null and b/public/assets/images/stickers/banny_party_2.png differ diff --git a/public/assets/images/stickers/banny_popcorn.png b/public/assets/images/stickers/banny_popcorn.png new file mode 100644 index 000000000..5b007e638 Binary files /dev/null and b/public/assets/images/stickers/banny_popcorn.png differ diff --git a/public/assets/images/stickers/banny_shoes.png b/public/assets/images/stickers/banny_shoes.png new file mode 100644 index 000000000..7b5aadec6 Binary files /dev/null and b/public/assets/images/stickers/banny_shoes.png differ diff --git a/public/assets/images/stickers/banny_stoned.png b/public/assets/images/stickers/banny_stoned.png new file mode 100644 index 000000000..80f3889ea Binary files /dev/null and b/public/assets/images/stickers/banny_stoned.png differ diff --git a/public/assets/images/stickers/banny_thumbsup.png b/public/assets/images/stickers/banny_thumbsup.png new file mode 100644 index 000000000..37a67a90b Binary files /dev/null and b/public/assets/images/stickers/banny_thumbsup.png differ diff --git a/public/assets/images/stickers/banny_wow.png b/public/assets/images/stickers/banny_wow.png new file mode 100644 index 000000000..633484cad Binary files /dev/null and b/public/assets/images/stickers/banny_wow.png differ diff --git a/public/assets/images/stickers/banny_yes.png b/public/assets/images/stickers/banny_yes.png new file mode 100644 index 000000000..4bb805355 Binary files /dev/null and b/public/assets/images/stickers/banny_yes.png differ diff --git a/public/assets/juice-logo-full_black.png b/public/assets/juice-logo-full_black.png new file mode 100644 index 000000000..e5b14a516 Binary files /dev/null and b/public/assets/juice-logo-full_black.png differ diff --git a/public/assets/juice-logo-full_black.svg b/public/assets/juice-logo-full_black.svg new file mode 100644 index 000000000..f88b63c2f --- /dev/null +++ b/public/assets/juice-logo-full_black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/juice-logo-full_white.png b/public/assets/juice-logo-full_white.png new file mode 100644 index 000000000..56792591f Binary files /dev/null and b/public/assets/juice-logo-full_white.png differ diff --git a/public/assets/juice-logo-full_white.svg b/public/assets/juice-logo-full_white.svg new file mode 100644 index 000000000..69a825e38 --- /dev/null +++ b/public/assets/juice-logo-full_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/juice-logo-icon_black.png b/public/assets/juice-logo-icon_black.png new file mode 100644 index 000000000..ec2fc8b9a Binary files /dev/null and b/public/assets/juice-logo-icon_black.png differ diff --git a/public/assets/juice_logo-od.png b/public/assets/juice_logo-od.png deleted file mode 100644 index 2cf989b78..000000000 Binary files a/public/assets/juice_logo-od.png and /dev/null differ diff --git a/public/assets/juice_logo-ol.png b/public/assets/juice_logo-ol.png deleted file mode 100644 index e2173a5a9..000000000 Binary files a/public/assets/juice_logo-ol.png and /dev/null differ diff --git a/public/assets/juice_logo-orange.svg b/public/assets/juice_logo-orange.svg new file mode 100644 index 000000000..acb9219cc --- /dev/null +++ b/public/assets/juice_logo-orange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/orange_lady-od.png b/public/assets/orange_lady-od.png deleted file mode 100644 index fd3979c3f..000000000 Binary files a/public/assets/orange_lady-od.png and /dev/null differ diff --git a/public/assets/orange_lady-ol.png b/public/assets/orange_lady-ol.png deleted file mode 100644 index a80e7f4cd..000000000 Binary files a/public/assets/orange_lady-ol.png and /dev/null differ diff --git a/public/assets/pina.png b/public/assets/pina.png deleted file mode 100644 index 853cc2977..000000000 Binary files a/public/assets/pina.png and /dev/null differ diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 000000000..8745a3cf6 --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #5777EB + + + diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 000000000..97fb90e51 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 000000000..63f27e011 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 43173a8af..f7940b7a7 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 70d034b07..000000000 --- a/public/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Juicebox - - - - -
- - diff --git a/public/info/1.txt b/public/info/1.txt deleted file mode 100644 index 70f3742e3..000000000 --- a/public/info/1.txt +++ /dev/null @@ -1,70 +0,0 @@ -Name: juicebox -Handle: @juicebox -Website: http://juicebox.money -Twitter: @juicedotwork - - - -Purpose ---------- -Juicebox develops the Juicebox protocol, and supports projects that are powered by Juicebox. - - - -Revenue plan ---------- -5% of all the $ETH that projects powered by Juicebox bring in runs through the Juicebox project. - - - -Funding target ---------- -$150k / 90 days - - - -Expenses ---------- -$30k / month | staff -3 founding/core contributors (twitter: @me_jango, @peripheralist, & @nMieos) - -$60k | growth -We need to aggressively show projects that they can be successfuly if they're powered by Juicebox. These funds allow us to pay other contributors from the community (@sageKellyn, @nervetrip, ...) who are working alongside the core team toward this end, and to give grants to promising projects using Juicebox. - - - -Reserve rate ---------- -5% - - - -Discount rate ---------- -95% - - - -Bonding curve ---------- -70% - - - -Product Roadmap ---------- -- Build L2 Juicebox terminals so projects can get paid on L2s. -- Design FundingCycleBallots so that projects can choose to hand off reconfiguration decisions to their ticket holders, etc. -- Build mods for all kinds of things. -- Transfer control of governance from the founding contributors to JUICE ticket holders. - -Check the project's Github and get on our Discord if you want to contribute. - - -https://github.com/juicebox-work/juicehouse -https://discord.gg/6jXrJSyDFf - - -🧃⚡️ - -Published: May 24, 2021 diff --git a/public/manifest.json b/public/manifest.json index 9b950ab48..5af500935 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,18 +1,22 @@ { - "short_name": "Juicebox", - "name": "Juicebox", - "description": "Community funding for people and projects.", - "iconPath": "android-chrome-144x144.png", - - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": "./index.html", - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file + "name": "Juicebox", + "short_name": "Juicebox", + "description": "The programmable funding protocol. Fund anything on Ethererum.", + "iconPath": "/assets/juice_logo-orange.svg", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + } + ], + "theme_color": "#5777EB", + "background_color": "#5777EB", + "display": "standalone", + "start_url": "." +} diff --git a/public/mstile-144x144.png b/public/mstile-144x144.png new file mode 100644 index 000000000..3bfbebdef Binary files /dev/null and b/public/mstile-144x144.png differ diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png new file mode 100644 index 000000000..e243d331b Binary files /dev/null and b/public/mstile-150x150.png differ diff --git a/public/mstile-310x150.png b/public/mstile-310x150.png new file mode 100644 index 000000000..1ced3f873 Binary files /dev/null and b/public/mstile-310x150.png differ diff --git a/public/mstile-310x310.png b/public/mstile-310x310.png new file mode 100644 index 000000000..2d7f52989 Binary files /dev/null and b/public/mstile-310x310.png differ diff --git a/public/mstile-70x70.png b/public/mstile-70x70.png new file mode 100644 index 000000000..7df3a66f3 Binary files /dev/null and b/public/mstile-70x70.png differ diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg new file mode 100644 index 000000000..c7684882c --- /dev/null +++ b/public/safari-pinned-tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest deleted file mode 100644 index b1f322ada..000000000 --- a/public/site.webmanifest +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-144x144.png", - "sizes": "144x144", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 000000000..1a3f455f8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1 @@ +NODE_ENV=production yarn install && yarn build \ No newline at end of file diff --git a/scripts/extract-translations-template.sh b/scripts/extract-translations-template.sh new file mode 100755 index 000000000..a0a93ee1d --- /dev/null +++ b/scripts/extract-translations-template.sh @@ -0,0 +1,4 @@ +# https://github.com/lingui/js-lingui/issues/952#issuecomment-765449330 +export NODE_ENV=development + +lingui extract-template \ No newline at end of file diff --git a/scripts/graphql-codegen-override-pv.js b/scripts/graphql-codegen-override-pv.js new file mode 100644 index 000000000..ec4d23770 --- /dev/null +++ b/scripts/graphql-codegen-override-pv.js @@ -0,0 +1,22 @@ +const fs = require('fs') + +const path = __dirname + '/../src/generated/graphql.tsx' + +try { + // Load generated file + let contents = fs.readFileSync(path).toString() + + // Replace generic string type with PV type for pv properties + contents = contents.replaceAll("pv: Scalars['String']", 'pv: PV') + contents = contents.replaceAll('pv: string', 'pv: PV') + + // Add import of PV type + contents = `import { PV } from 'models/pv';\n` + contents + + // Overwrite original file + fs.writeFileSync(path, contents) + + console.info('✔ PV types overwritten in generated graphql types') +} catch (e) { + console.error('Error overriding PV types in generated graphql types', e) +} diff --git a/scripts/lint-translations-template.sh b/scripts/lint-translations-template.sh new file mode 100755 index 000000000..d7bf5c102 --- /dev/null +++ b/scripts/lint-translations-template.sh @@ -0,0 +1,24 @@ +# extract new strings +yarn i18n:extract + +POT_FILE="src/locales/messages.pot" + +# get all translation files that were updated +# from `yarn i18n:extract` +FILE_DIFF=$(git diff --name-only $POT_FILE) + +# Bail if there were changes to the template file. +# that weren't included in the commit. +if ! [ -z "$FILE_DIFF" ]; then + echo "🍎 messages.pot is out of date. Run 'yarn i18n:extract' locally and commit the changes." + exit 1 +else + # bail if pot file contain git merge conflict diff artefact (like <<<<< HEAD) + if grep -e '<<<' -e '>>>' -e '====' "$POT_FILE"; then + echo "🍎 messages.pot contains artifacts from git merge conflict." + exit 1 + fi + + echo "🍏 messages.pot is up to date." + exit 0 +fi \ No newline at end of file diff --git a/sentry.client.config.js b/sentry.client.config.js new file mode 100644 index 000000000..3f91ac0dd --- /dev/null +++ b/sentry.client.config.js @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the browser. +// The config you add here will be used whenever a page is visited. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn: SENTRY_DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1.0, + // ... + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}) diff --git a/sentry.edge.config.js b/sentry.edge.config.js new file mode 100644 index 000000000..987ac85d7 --- /dev/null +++ b/sentry.edge.config.js @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever middleware or an Edge route handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn: SENTRY_DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1.0, + // ... + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}) diff --git a/sentry.properties b/sentry.properties new file mode 100644 index 000000000..7a2b5c6b5 --- /dev/null +++ b/sentry.properties @@ -0,0 +1,4 @@ +defaults.url=https://sentry.io/ +defaults.org=peel-dao +defaults.project=juicebox-money +cli.executable=node_modules/@sentry/cli/bin/sentry-cli diff --git a/sentry.server.config.js b/sentry.server.config.js new file mode 100644 index 000000000..4b244f5b5 --- /dev/null +++ b/sentry.server.config.js @@ -0,0 +1,17 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn: SENTRY_DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1.0, + // ... + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}) diff --git a/src/Network.tsx b/src/Network.tsx deleted file mode 100644 index 4cc70cbe0..000000000 --- a/src/Network.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { Web3Provider } from '@ethersproject/providers' -import { NETWORKS } from 'constants/networks' -import { NetworkContext } from 'contexts/networkContext' -import { ChildElems } from 'models/child-elems' -import { NetworkName } from 'models/network-name' -import { useContext, useEffect, useState } from 'react' -import { readNetwork } from 'constants/networks' -import { initOnboard } from 'utils/onboard' -import { API, Subscriptions, Wallet } from 'bnc-onboard/dist/src/interfaces' -import { ThemeContext } from 'contexts/themeContext' - -const KEY_SELECTED_WALLET = 'selectedWallet' - -export default function Network({ children }: { children: ChildElems }) { - const { isDarkMode } = useContext(ThemeContext) - - const [signingProvider, setSigningProvider] = useState() - const [network, setNetwork] = useState() - const [account, setAccount] = useState() - const [onboard, setOnboard] = useState() - - const resetWallet = () => { - onboard?.walletReset() - setSigningProvider(undefined) - window.localStorage.setItem(KEY_SELECTED_WALLET, '') - } - - const selectWallet = async () => { - resetWallet() - - // Open select wallet modal. - const selectedWallet = await onboard?.walletSelect() - - // User quit modal. - if (!selectedWallet) { - return - } - - // Wait for wallet selection initialization - await onboard?.walletCheck() - } - - const logOut = async () => { - resetWallet() - } - - const initializeWallet = () => { - if (onboard) return - - const selectWallet = async (newWallet: Wallet) => { - if (newWallet.provider) { - // Reset the account when a new wallet is connected, as it will be resolved by the provider. - setAccount(undefined) - window.localStorage.setItem(KEY_SELECTED_WALLET, newWallet.name || '') - setSigningProvider(new Web3Provider(newWallet.provider)) - } else { - resetWallet() - } - } - const config: Subscriptions = { - address: setAccount, - wallet: selectWallet, - } - setOnboard(initOnboard(config, isDarkMode)) - } - - const onDarkModeChanged = () => { - if (onboard) { - onboard.config({ darkMode: isDarkMode }) - } - } - - const refreshNetwork = () => { - async function getNetwork() { - await signingProvider?.ready - - const network = signingProvider?.network?.chainId - ? NETWORKS[signingProvider.network.chainId] - : undefined - - setNetwork(network?.name) - } - getNetwork() - } - - const reconnectWallet = () => { - const previouslySelectedWallet = - window.localStorage.getItem(KEY_SELECTED_WALLET) - if (previouslySelectedWallet && onboard) { - onboard.walletSelect(previouslySelectedWallet) - } - } - - useEffect(initializeWallet, []) - useEffect(onDarkModeChanged, [isDarkMode]) - useEffect(refreshNetwork, [signingProvider, network]) - useEffect(reconnectWallet, [onboard]) - - return ( - - {children} - - ) -} diff --git a/src/Theme.tsx b/src/Theme.tsx deleted file mode 100644 index 1ddf99065..000000000 --- a/src/Theme.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ThemeContext } from 'contexts/themeContext' -import { useJuiceTheme } from 'hooks/JuiceTheme' -import { ChildElems } from 'models/child-elems' - -export default function Theme({ children }: { children: ChildElems }) { - const juiceTheme = useJuiceTheme() - - return ( - {children} - ) -} diff --git a/src/User.tsx b/src/User.tsx deleted file mode 100644 index 43e630cc4..000000000 --- a/src/User.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { BigNumber } from '@ethersproject/bignumber' -import { UserContext } from 'contexts/userContext' -import { useContractLoader } from 'hooks/ContractLoader' -import { useGasPrice } from 'hooks/GasPrice' -import { useTransactor } from 'hooks/Transactor' -import { ChildElems } from 'models/child-elems' -import { useEffect, useState } from 'react' - -export default function User({ children }: { children: ChildElems }) { - const [adminFeePercent, setAdminFeePercent] = useState() - const contracts = useContractLoader() - - const gasPrice = useGasPrice('average') - - const transactor = useTransactor({ - gasPrice: gasPrice ? BigNumber.from(gasPrice) : undefined, - }) - - useEffect(() => { - contracts?.TerminalV1.functions.fee().then((res: [BigNumber]) => { - if (!adminFeePercent?.eq(res[0])) setAdminFeePercent(res[0]) - }) - }, [setAdminFeePercent, contracts]) - - return ( - - {children} - - ) -} diff --git a/src/__mocks__/@lingui/core.js b/src/__mocks__/@lingui/core.js new file mode 100644 index 000000000..bea09096f --- /dev/null +++ b/src/__mocks__/@lingui/core.js @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +const i18n = { + load: () => {}, + loadLocaleData: () => {}, + activate: () => {}, + _: ({ message, values }) => + message + ? message.replace(/{([A-Za-z0-9]+)}/g, (str, prop) => values[prop]) + : '', +} +export { i18n } diff --git a/src/__mocks__/@lingui/detect-locale.js b/src/__mocks__/@lingui/detect-locale.js new file mode 100644 index 000000000..9d808fc25 --- /dev/null +++ b/src/__mocks__/@lingui/detect-locale.js @@ -0,0 +1,5 @@ +const detect = () => 'en', + fromUrl = () => null, + fromNavigator = () => null, + fromStorage = () => null +export { detect, fromNavigator, fromStorage, fromUrl } diff --git a/src/__mocks__/@lingui/react.js b/src/__mocks__/@lingui/react.js new file mode 100644 index 000000000..7b1d3c5db --- /dev/null +++ b/src/__mocks__/@lingui/react.js @@ -0,0 +1,15 @@ +const Trans = function ({ message, values }) { + let replacedMessage = message + for (const key in values) { + // eslint-disable-next-line no-prototype-builtins + if (values.hasOwnProperty(key)) { + const regex = new RegExp(`\\{${key}\\}`, 'g') + replacedMessage = replacedMessage.replace(regex, values[key]) + } + } + return replacedMessage + }, + I18nProvider = function ({ children }) { + return children + } +export { Trans, I18nProvider } diff --git a/src/components/AMMPrices/TokenAMMPriceRow.tsx b/src/components/AMMPrices/TokenAMMPriceRow.tsx new file mode 100644 index 000000000..02b8f9535 --- /dev/null +++ b/src/components/AMMPrices/TokenAMMPriceRow.tsx @@ -0,0 +1,92 @@ +import { LoadingOutlined } from '@ant-design/icons' +import { t, Trans } from '@lingui/macro' +import { Tooltip } from 'antd' +import SushiswapLogo from 'components/icons/Sushiswap' +import UniswapLogo from 'components/icons/Uniswap' +import { ONE_TRILLION } from 'constants/numbers' +import { classNames } from 'utils/classNames' +import { formatOrTruncate } from 'utils/format/formatNumber' + +import ExternalLink from '../ExternalLink' + +import TooltipIcon from '../TooltipIcon' + +type ExchangeName = 'Uniswap' | 'Sushiswap' + +const LOGOS = { + Uniswap: UniswapLogo, + Sushiswap: SushiswapLogo, +} + +type Props = { + className?: string + exchangeName: ExchangeName + tokenSymbol: string + exchangeLink?: string + WETHPrice?: string + loading?: boolean +} + +export default function TokenAMMPriceRow({ + className, + exchangeName, + tokenSymbol, + exchangeLink, + WETHPrice, + loading, +}: Props) { + const LogoComponent = LOGOS[exchangeName] + + const NotAvailableText = () => { + const tooltip = !WETHPrice + ? t`${exchangeName} has no market for ${tokenSymbol}.` + : '' + + return ( + + + {!WETHPrice ? Unavailable : null} + + + + ) + } + + const formatPrice = (price: string) => { + const p = parseInt(price, 10) + const formatLimit = ONE_TRILLION + + // format all values below trillion value, otherwise truncate is as long number. + return formatOrTruncate(p, formatLimit) + } + + return ( +
+
+ + + + {exchangeName} +
+ {loading && } + + {!loading && + (WETHPrice ? ( + + + {`${formatPrice(WETHPrice)} ${tokenSymbol}/1 ETH`} + + + ) : ( + + ))} +
+ ) +} diff --git a/src/components/AMMPrices/hooks/useERC20SushiswapPrice.ts b/src/components/AMMPrices/hooks/useERC20SushiswapPrice.ts new file mode 100644 index 000000000..13af4a7df --- /dev/null +++ b/src/components/AMMPrices/hooks/useERC20SushiswapPrice.ts @@ -0,0 +1,65 @@ +import { CurrencyAmount, Pair, Route, Token, WETH9 } from '@sushiswap/sdk' +import { Contract } from 'ethers' + +import IUniswapV2PairABI from '@uniswap/v2-core/build/IUniswapV2Pair.json' + +import { useQuery } from 'react-query' + +import { readNetwork } from 'constants/networks' + +import { WAD_DECIMALS } from 'constants/numbers' +import { readProvider } from 'constants/readProvider' + +/** + * Fetches information about a pair and constructs a pair from the given two tokens. + * @param tokenA first token + * @param tokenB second token + * @param provider the provider to use to fetch the data + * Source: https://github.com/Uniswap/v2-sdk/blob/a88048e9c4198a5bdaea00883ca00c8c8e582605/src/fetcher.ts + */ +async function fetchPairData(tokenA: Token, tokenB: Token): Promise { + const pairAddress = Pair.getAddress(tokenA, tokenB) + + const [reserves0, reserves1] = await new Contract( + pairAddress, + IUniswapV2PairABI.abi, + readProvider, + ).getReserves() + + const balances = tokenA.sortsBefore(tokenB) + ? [reserves0, reserves1] + : [reserves1, reserves0] + return new Pair( + CurrencyAmount.fromRawAmount(tokenA, balances[0]), + CurrencyAmount.fromRawAmount(tokenB, balances[1]), + ) +} + +type Props = { + tokenSymbol: string + tokenAddress: string +} + +const networkId = readNetwork.chainId + +export function useSushiswapPriceQuery({ tokenSymbol, tokenAddress }: Props) { + const PROJECT_TOKEN = new Token( + networkId, + tokenAddress, + WAD_DECIMALS, + tokenSymbol, + ) + const WETH = WETH9[networkId] + + return useQuery([`${tokenSymbol}-sushiswap-price`], async () => { + // note that you may want/need to handle this async code differently, + // for example if top-level await is not an option + const pair = await fetchPairData(PROJECT_TOKEN, WETH) + + const route = new Route([pair], WETH, PROJECT_TOKEN) + return { + tokenSymbol, + midPrice: route.midPrice, + } + }) +} diff --git a/src/components/AMMPrices/hooks/useERC20UniswapPrice.ts b/src/components/AMMPrices/hooks/useERC20UniswapPrice.ts new file mode 100644 index 000000000..1e58a25b1 --- /dev/null +++ b/src/components/AMMPrices/hooks/useERC20UniswapPrice.ts @@ -0,0 +1,184 @@ +import { Token } from '@uniswap/sdk-core' +import IUniswapV3FactoryABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json' +import IUniswapV3PoolABI from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json' +import { + Pool, + FACTORY_ADDRESS as UNISWAP_V3_FACTORY_ADDRESS, +} from '@uniswap/v3-sdk' +import { BigNumber, Contract } from 'ethers' +import { useQuery } from 'react-query' + +import { WETH } from 'constants/contracts/tokens' +import { readNetwork } from 'constants/networks' +import { WAD_DECIMALS } from 'constants/numbers' +import { readProvider } from 'constants/readProvider' +import { isZeroAddress } from 'utils/address' + +interface Immutables { + factory: string + token0: string + token1: string + fee: number + tickSpacing: number + maxLiquidityPerTick: BigNumber +} + +interface State { + liquidity: BigNumber + sqrtPriceX96: BigNumber + tick: number + observationIndex: number + observationCardinality: number + observationCardinalityNext: number + feeProtocol: number + unlocked: boolean +} + +type Props = { + tokenSymbol: string + tokenAddress: string +} + +/** + * Pools are created at a specific fee tier. + * https://docs.uniswap.org/protocol/concepts/V3-overview/fees#pool-fees-tiers + */ +const UNISWAP_FEES_BPS = [10000, 3000, 500] +const networkId = readNetwork.chainId + +/** + * Hook to fetch the Uniswap price for a given token. + * Uniswap-related code inspired by https://docs.uniswap.org/sdk/guides/fetching-prices. + */ +export function useUniswapPriceQuery({ tokenSymbol, tokenAddress }: Props) { + const factoryContract = new Contract( + UNISWAP_V3_FACTORY_ADDRESS, + IUniswapV3FactoryABI.abi, + readProvider, + ) + + /** + * Recursively attempts to find liquidty pool at a given [fee]. + * Recurs through each fee tier until a pool is found. + * If no pool is found, return undefined. + * @returns contract address of liquidty pool + */ + const getPoolAddress = async ( + fee: number | undefined = UNISWAP_FEES_BPS[0], + ): Promise => { + const poolAddress = await factoryContract.getPool(tokenAddress, WETH, fee) + + if (poolAddress && !isZeroAddress(poolAddress)) { + return poolAddress + } + + // If we've got no more fees to search on, bail. + const feeIdx = UNISWAP_FEES_BPS.findIndex(f => f === fee) + if (feeIdx === UNISWAP_FEES_BPS.length - 1) { + return undefined + } + + return getPoolAddress(UNISWAP_FEES_BPS[feeIdx + 1]) + } + + async function getPoolImmutables(poolContract: Contract) { + const [factory, token0, token1, fee, tickSpacing, maxLiquidityPerTick] = + await Promise.all([ + poolContract.factory(), + poolContract.token0(), + poolContract.token1(), + poolContract.fee(), + poolContract.tickSpacing(), + poolContract.maxLiquidityPerTick(), + ]) + + const immutables: Immutables = { + factory, + token0, + token1, + fee, + tickSpacing, + maxLiquidityPerTick, + } + + return immutables + } + + async function getPoolState(poolContract: Contract) { + const [slot, liquidity] = await Promise.all([ + poolContract.slot0(), + poolContract.liquidity(), + ]) + const PoolState: State = { + liquidity, + sqrtPriceX96: slot[0], + tick: slot[1], + observationIndex: slot[2], + observationCardinality: slot[3], + observationCardinalityNext: slot[4], + feeProtocol: slot[5], + unlocked: slot[6], + } + + return PoolState + } + + return useQuery( + [`${tokenSymbol}-uniswap-price`], + async () => { + try { + const poolAddress = await getPoolAddress() + if (!poolAddress) { + throw new Error('No liquidity pool found.') + } + + const poolContract = new Contract( + poolAddress, + IUniswapV3PoolABI.abi, + readProvider, + ) + + const [immutables, state] = await Promise.all([ + getPoolImmutables(poolContract), + getPoolState(poolContract), + ]) + + const PROJECT_TOKEN = new Token( + networkId, + immutables.token0, + WAD_DECIMALS, + tokenSymbol, + ) + const WETH = new Token( + networkId, + immutables.token1, + WAD_DECIMALS, + 'WETH', + ) + + const projectTokenETHPool = new Pool( + PROJECT_TOKEN, + WETH, + immutables.fee, + state.sqrtPriceX96.toString(), + state.liquidity.toString(), + state.tick, + ) + + const projectTokenPrice = projectTokenETHPool.token0Price + const WETHPrice = projectTokenETHPool.token1Price + return { + tokenSymbol, + projectTokenPrice, + WETHPrice, + liquidity: projectTokenETHPool.liquidity.toString(), + } + } catch (e) { + console.error('Error fetching AMM price', e) + } + }, + { + refetchInterval: 30000, // refetch every 30 seconds + }, + ) +} diff --git a/src/components/AMMPrices/index.tsx b/src/components/AMMPrices/index.tsx new file mode 100644 index 000000000..2d46e3931 --- /dev/null +++ b/src/components/AMMPrices/index.tsx @@ -0,0 +1,60 @@ +import { Trans } from '@lingui/macro' +import { generateAMMLink } from 'lib/amm' +import { useSushiswapPriceQuery } from './hooks/useERC20SushiswapPrice' +import { useUniswapPriceQuery } from './hooks/useERC20UniswapPrice' + +import TokenAMMPriceRow from './TokenAMMPriceRow' + +type Props = { + mode: 'buy' | 'redeem' + tokenSymbol: string + tokenAddress: string +} + +/** + * Component for rendering a set of AMM Prices. + */ +export default function AMMPrices({ mode, tokenSymbol, tokenAddress }: Props) { + const { data: uniswapPriceData, isLoading: uniswapLoading } = + useUniswapPriceQuery({ + tokenSymbol, + tokenAddress, + }) + + const { data: sushiswapPriceData, isLoading: sushiswapLoading } = + useSushiswapPriceQuery({ + tokenSymbol, + tokenAddress, + }) + + return ( + <> +

+ Current 3rd Party Exchange Rates +

+ + + + ) +} diff --git a/src/components/AboutDashboard/AboutDashboard.tsx b/src/components/AboutDashboard/AboutDashboard.tsx new file mode 100644 index 000000000..33f844c66 --- /dev/null +++ b/src/components/AboutDashboard/AboutDashboard.tsx @@ -0,0 +1,25 @@ +import { Footer } from 'components/Footer' +import { AboutTheProtocolSection } from './components/AboutTheProtocolSection' +import { BuiltByTheBestSection } from './components/BuiltByTheBestSection' +import { FindOutMoreSection } from './components/FindOutMoreSection' +import { HeroSection } from './components/HeroSection' +import { JuiceboxDaoSection } from './components/JuiceboxDaoSection' +import { OurMissionSection } from './components/OurMissionSection' +import { WhatDoWeValueSection } from './components/WhatDoWeValueSection' + +export const AboutDashboard = () => { + return ( + <> +
+ + + + + + + +
+