├── .gitignore
├── README.md
├── docs
├── logo-square.png
└── logo.png
├── polynote-backend
├── .env.example
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── docker-compose.example.yaml
├── nest-cli.json
├── package-lock.json
├── package.json
├── publish.sh
├── src
│ ├── app.module.ts
│ ├── collections
│ │ ├── note.collection.ts
│ │ └── user.collection.ts
│ ├── config.ts
│ ├── main.ts
│ ├── middlewares
│ │ └── AuthMiddleware.ts
│ ├── modules
│ │ ├── ai
│ │ │ ├── ai.controller.ts
│ │ │ ├── ai.dto.ts
│ │ │ ├── ai.module.ts
│ │ │ └── ai.service.ts
│ │ ├── listener
│ │ │ ├── listener.controller.ts
│ │ │ └── listener.module.ts
│ │ ├── note
│ │ │ ├── note.controller.ts
│ │ │ ├── note.dto.ts
│ │ │ ├── note.module.ts
│ │ │ └── note.service.ts
│ │ ├── upload
│ │ │ ├── upload.controller.ts
│ │ │ ├── upload.module.ts
│ │ │ └── upload.service.ts
│ │ └── user
│ │ │ ├── user.controller.ts
│ │ │ ├── user.dto.ts
│ │ │ ├── user.module.ts
│ │ │ └── user.service.ts
│ └── utils
│ │ ├── abi.ts
│ │ ├── checkAdmin.ts
│ │ ├── formatAddress.ts
│ │ ├── getPolybaseInstance.ts
│ │ ├── getRpcProvider.ts
│ │ ├── getTimestamp.ts
│ │ ├── getTokenData.ts
│ │ ├── index.ts
│ │ ├── initPolybase.ts
│ │ ├── messages.ts
│ │ ├── numbers.ts
│ │ ├── signature.ts
│ │ ├── verifyK1Signature.ts
│ │ ├── verifyR1Signature.ts
│ │ └── verifySignature.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
├── polynote-blockchain
├── .gitignore
├── LICENSE
├── README.md
├── contracts
│ └── Polynote.sol
├── deployments.ts
├── foundry.toml
├── hardhat.config.ts
├── package-lock.json
├── package.json
├── scripts
│ └── deploy.ts
├── test
│ └── Polynote.ts
├── tsconfig.json
└── yarn.lock
├── polynote-frontend
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── README.md
├── assets
│ ├── connect-page-illustration.png
│ ├── empty-state-illustration.png
│ ├── features-illustration-light.png
│ ├── features-illustration.png
│ ├── flying.png
│ ├── landing-illustrations.png
│ ├── logo
│ │ ├── logo-large-white.png
│ │ ├── logo-large.png
│ │ └── logo-small.png
│ ├── partners
│ │ ├── filecoin.png
│ │ ├── nest.png
│ │ ├── polybase.png
│ │ ├── push.png
│ │ ├── react.png
│ │ ├── scroll.png
│ │ └── tiptap.png
│ ├── screens-light.png
│ ├── screens-sm-light.png
│ ├── screens-sm.png
│ ├── screens.png
│ ├── team
│ │ ├── alim.png
│ │ ├── bugra.png
│ │ ├── eylul.png
│ │ ├── farhad.png
│ │ └── taylan.png
│ └── wallpaper.png
├── components
│ ├── AccountModal
│ │ └── AccountModal.tsx
│ ├── AiModal
│ │ └── AiModal.tsx
│ ├── Container
│ │ ├── Container.module.scss
│ │ └── Container.tsx
│ ├── CreateNoteModal
│ │ └── CreateNoteModal.tsx
│ ├── Editor
│ │ ├── BubbleMenuEditor.tsx
│ │ ├── Editor.tsx
│ │ └── FloatingMenuEditor.tsx
│ ├── Header
│ │ └── Header.tsx
│ ├── Main
│ │ └── Main.tsx
│ ├── Navbar
│ │ └── Navbar.tsx
│ ├── NoteEditor
│ │ └── NoteEditor.tsx
│ ├── NoteHeader
│ │ └── NoteHeader.tsx
│ ├── SettingsModal
│ │ └── SettingsModal.tsx
│ ├── ShareModal
│ │ └── ShareModal.tsx
│ ├── Sidebar
│ │ └── Sidebar.tsx
│ └── index.ts
├── config.ts
├── consts
│ ├── abi.ts
│ ├── chains.ts
│ ├── contracts.ts
│ ├── index.ts
│ ├── messages.ts
│ ├── numbers.ts
│ ├── paths.ts
│ ├── storage.ts
│ └── upload.ts
├── hooks
│ ├── useAvatar.ts
│ ├── useContractFunction.ts
│ ├── useCopyText.ts
│ ├── useDebounce.ts
│ ├── useDropdown.ts
│ ├── useInitializeTheme.tsx
│ ├── useListenNetworkChange.ts
│ ├── useModal.ts
│ ├── useNotify.ts
│ ├── useOnAccountsChange.ts
│ ├── useOnClickOutside.tsx
│ └── usePopper.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── _app.tsx
│ ├── connect.tsx
│ ├── dashboard.tsx
│ ├── index.tsx
│ └── shared
│ │ └── [note_id].tsx
├── postcss.config.js
├── public
│ └── favicon.ico
├── recoil
│ ├── notes
│ │ ├── NotesStore.ts
│ │ ├── NotesStoreHooks.ts
│ │ └── types.ts
│ ├── theme
│ │ ├── ThemeStore.ts
│ │ ├── ThemeStoreHooks.ts
│ │ └── types.ts
│ └── user
│ │ ├── UserStore.ts
│ │ └── UserStoreHooks.ts
├── restapi
│ ├── index.ts
│ ├── queries
│ │ ├── index.ts
│ │ ├── useAiMutation.ts
│ │ ├── useAuthUserMutation.ts
│ │ ├── useCreateNoteMutation.ts
│ │ ├── useCreatePolybaseUserMutation.ts
│ │ ├── useDeleteNoteMutation.ts
│ │ ├── useNotesQuery.ts
│ │ ├── useOptInMutation.ts
│ │ ├── useOptOutMutation.ts
│ │ ├── usePolybaseUserQuery.ts
│ │ ├── usePushNotifications.ts
│ │ ├── useUpdateNoteMutation.ts
│ │ ├── useUpdatePolybaseUserNameMutation.ts
│ │ └── useUploadMutation.ts
│ └── types.ts
├── styles
│ └── globals.scss
├── tailwind.config.js
├── tsconfig.json
├── types
│ └── index.ts
├── ui
│ ├── Button
│ │ ├── Button.tsx
│ │ └── CustomConnectButton.tsx
│ ├── Input
│ │ └── Input.tsx
│ ├── Modal
│ │ ├── Modal.module.scss
│ │ └── Modal.tsx
│ ├── Spinner
│ │ ├── Spinner.module.scss
│ │ └── Spinner.tsx
│ ├── Typography
│ │ ├── Typography.module.scss
│ │ └── Typography.tsx
│ └── index.ts
└── utils
│ ├── AiModalContext.tsx
│ ├── clsnm.ts
│ ├── customWallets.ts
│ ├── formatAddress.ts
│ ├── formatRPCErrorMessage.ts
│ ├── getAvatar.ts
│ ├── getPolybaseUserName.ts
│ ├── getSharedMessage.ts
│ ├── getToken.ts
│ └── signature.ts
└── polynote-zksync-contracts
├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── contracts
└── Polynote.sol
├── deploy
├── deploy.ts
└── utils.ts
├── hardhat.config.ts
├── package-lock.json
├── package.json
└── test
├── erc20
└── myerc20token.test.ts
├── greeter.test.ts
└── nft
└── mynft.test.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Polynote
2 |
3 | Private note taking application #buidl on [Polybase](https://polybase.xyz/) for [ETHGlobal Scaling 2023 Hackathon](https://ethglobal.com/events/scaling2023).
4 |
5 |
6 |
7 | ## Project description
8 |
9 | Polybase is a great tool for storing your scaling data on Web3. Our main goal is to create an application which reaches to end-users and used in daily basis. We decided to build a private note taking application on Polybase. The users will connect their wallets and start taking notes, which are going to be written to Polybase collections after being encrypted. Users will be able to whitelist wallets through a smart contract which is going to be deployed on Zksync. We are sending a push notification to shared addresses through Push SDK to let them know about the note link. Our text editor supports various features like image upload, text highlighting, headlines, code blocks, and more.
10 |
11 | ## zkSync Smart Contract
12 |
13 | [0x29e362244AB911d7Adc78dc08561a1C514D9096C](https://sepolia.explorer.zksync.io/address/0x7e615C0ad8C5BD894e42d3E3c7b0C533dFfC1cA0)
14 |
15 | ## Build
16 |
17 | **Build frontend**
18 |
19 | ```bash
20 | $ cd ./polynote-frontend
21 | $ npm install # Install dependencies
22 | $ npm run dev # Run development server
23 | ```
24 |
25 | **Build backend**
26 |
27 | **Step 1 - Create docker compose**
28 |
29 | 1. Create `docker-compose.yaml` on `./polynote-backend` directory.
30 |
31 | 2. Copy the file below and fill environment section
32 |
33 | ```
34 | version: '3.8'
35 | services:
36 | polynote-api:
37 | image: asgarovfarhad/polynote-api:1.0.0
38 | container_name: polynote-api
39 | build:
40 | context: .
41 | dockerfile: Dockerfile
42 | ports:
43 | - 8000:8000
44 | environment:
45 | - APP_CORS=*
46 | - APP_PORT=8000
47 | - API_VERSION=1
48 | - APP_VERSION=development
49 | - PRIVATE_KEY= # Your private key
50 | - ENCRYPTION_KEY=[] # Read more at https://polybase.xyz/docs/encrypt-data#encrypt-data-using-symmetric-encryption
51 | - NETWORK_RPC_URL=https://sepolia.era.zksync.dev
52 | - POLYNOTE_CONTRACT= # Your zksync contract address (Default is 0x29e362244AB911d7Adc78dc08561a1C514D9096C)
53 | - DB_NAMESPACE= # Your unique namespace string (default is polynote)
54 | - WEB3_STORAGE_TOKEN= # Read more at https://web3.storage/
55 | - OPEN_AI_KEY=sk-
56 | platform: linux/amd64
57 | restart: always
58 | ```
59 |
60 | ### Note
61 |
62 | The https://web3.storage/ is under maintenance after sunset and we are working on integrating new APIs.
63 |
64 | **Step 2 - Run Docker**
65 |
66 | ```bash
67 | $ docker compose build
68 | $ docker compose up -d
69 | ```
70 |
71 | The server will run on port specified in `docker-compose.yaml`, which defaults to 8000.
72 |
73 | ## Tech Stack
74 |
75 | | Tech | Field |
76 | | ---------------------------------- | --------------------- |
77 | | [Polybase](https://polybase.xyz/) | Web3 Database |
78 | | [Filecoin](https://filecoin.io/) | Decentralized Storage |
79 | | [NestJS](https://nestjs.com/) | Backend |
80 | | [ReactJS](https://react.dev/) | Frontend |
81 | | [Hardhat](https://hardhat.org/) | Smart Contracts |
82 | | [Zksync](https://zksync.io/) | L2 |
83 | | [Push Protocol](https://push.org/) | Notification service |
84 | | [TiptapJS](https://tiptap.dev/) | Text Editor |
85 |
--------------------------------------------------------------------------------
/docs/logo-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/docs/logo-square.png
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/docs/logo.png
--------------------------------------------------------------------------------
/polynote-backend/.env.example:
--------------------------------------------------------------------------------
1 | APP_CORS=*
2 | APP_PORT=8000
3 | PRIVATE_KEY=
4 | API_VERSION=1
5 | ENCRYPTION_KEY=[]
--------------------------------------------------------------------------------
/polynote-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir : __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/polynote-backend/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | # Tests
18 | /coverage
19 | /.nyc_output
20 |
21 | # IDEs and editors
22 | /.idea
23 | .project
24 | .classpath
25 | .c9/
26 | *.launch
27 | .settings/
28 | *.sublime-workspace
29 |
30 | # IDE - VSCode
31 | .vscode/*
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
36 |
37 | .env
38 | docker-compose.yaml
--------------------------------------------------------------------------------
/polynote-backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/polynote-backend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Building layer
2 | FROM node:16-alpine as development
3 |
4 | # Optional NPM automation (auth) token build argument
5 | # ARG NPM_TOKEN
6 |
7 | # Optionally authenticate NPM registry
8 | # RUN npm set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
9 |
10 | WORKDIR /app
11 |
12 | # Copy configuration files
13 | COPY tsconfig*.json ./
14 | COPY package*.json ./
15 |
16 | # Install dependencies from package-lock.json, see https://docs.npmjs.com/cli/v7/commands/npm-ci
17 | RUN npm ci
18 |
19 | # Copy application sources (.ts, .tsx, js)
20 | COPY src/ src/
21 |
22 | # Build application (produces dist/ folder)
23 | RUN npm run build
24 |
25 | # Runtime (production) layer
26 | FROM node:16-alpine as production
27 |
28 | # Optional NPM automation (auth) token build argument
29 | # ARG NPM_TOKEN
30 |
31 | # Optionally authenticate NPM registry
32 | # RUN npm set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
33 |
34 | WORKDIR /app
35 |
36 | # Copy dependencies files
37 | COPY package*.json ./
38 |
39 | # Install runtime dependecies (without dev/test dependecies)
40 | RUN npm ci --omit=dev
41 |
42 | # Copy production build
43 | COPY --from=development /app/dist/ ./dist/
44 |
45 | # Expose application port
46 | EXPOSE 8000
47 |
48 | # Start application
49 | CMD [ "node", "dist/main.js" ]
--------------------------------------------------------------------------------
/polynote-backend/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
28 |
29 | ## Installation
30 |
31 | ```bash
32 | $ npm install
33 | ```
34 |
35 | ## Running the app
36 |
37 | ```bash
38 | # development
39 | $ npm run start
40 |
41 | # watch mode
42 | $ npm run start:dev
43 |
44 | # production mode
45 | $ npm run start:prod
46 | ```
47 |
48 | ## Test
49 |
50 | ```bash
51 | # unit tests
52 | $ npm run test
53 |
54 | # e2e tests
55 | $ npm run test:e2e
56 |
57 | # test coverage
58 | $ npm run test:cov
59 | ```
60 |
61 | ## Support
62 |
63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
64 |
65 | ## Stay in touch
66 |
67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
68 | - Website - [https://nestjs.com](https://nestjs.com/)
69 | - Twitter - [@nestframework](https://twitter.com/nestframework)
70 |
71 | ## License
72 |
73 | Nest is [MIT licensed](LICENSE).
74 |
--------------------------------------------------------------------------------
/polynote-backend/docker-compose.example.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | polynote-api:
4 | image: asgarovfarhad/polynote-api:1.0.0
5 | container_name: polynote-api
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | ports:
10 | - 8000:8000
11 | environment:
12 | - APP_CORS=*
13 | - APP_PORT=8000
14 | - API_VERSION=1
15 | - APP_VERSION=development
16 | - PRIVATE_KEY=
17 | - ENCRYPTION_KEY=[]
18 | - NETWORK_RPC_URL=https://alpha-rpc.scroll.io/l2
19 | - POLYNOTE_CONTRACT=
20 | - DB_NAMESPACE=polybase
21 | - WEB3_STORAGE_TOKEN=
22 | - OPEN_AI_KEY=sk-
23 | platform: linux/amd64
24 | restart: always
25 |
--------------------------------------------------------------------------------
/polynote-backend/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/polynote-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polynote-api",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/main",
16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:cov": "jest --coverage",
20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21 | "test:e2e": "jest --config ./test/jest-e2e.json"
22 | },
23 | "dependencies": {
24 | "@etherspot/eip1271-verification-util": "^0.1.2",
25 | "@nestjs/common": "9.0.0",
26 | "@nestjs/config": "2.2.0",
27 | "@nestjs/core": "9.0.0",
28 | "@nestjs/jwt": "10.0.2",
29 | "@nestjs/platform-express": "9.0.0",
30 | "@nestjs/swagger": "6.2.1",
31 | "@nestjs/typeorm": "9.0.1",
32 | "@polybase/client": "0.3.36",
33 | "@pushprotocol/restapi": "0.8.3",
34 | "buffer": "^6.0.3",
35 | "class-transformer": "0.5.1",
36 | "class-validator": "0.14.0",
37 | "csurf": "1.11.0",
38 | "elliptic": "^6.5.4",
39 | "ethers": "5.7.2",
40 | "helmet": "6.0.1",
41 | "minimist": "1.2.8",
42 | "mysql2": "2.3.3",
43 | "openai": "4.28.4",
44 | "process": "0.11.10",
45 | "reflect-metadata": "0.1.13",
46 | "rimraf": "3.0.2",
47 | "rxjs": "7.2.0",
48 | "typeorm": "0.3.11",
49 | "uuid": "9.0.0",
50 | "web3.storage": "4.5.4",
51 | "zksync-web3": "^0.14.3"
52 | },
53 | "devDependencies": {
54 | "@nestjs/cli": "9.0.0",
55 | "@nestjs/schematics": "9.0.0",
56 | "@nestjs/testing": "9.0.0",
57 | "@types/csurf": "1.11.2",
58 | "@types/elliptic": "^6.4.16",
59 | "@types/express": "4.17.13",
60 | "@types/jest": "28.1.8",
61 | "@types/multer": "^1.4.7",
62 | "@types/node": "16.0.0",
63 | "@types/supertest": "2.0.11",
64 | "@types/uuid": "^9.0.1",
65 | "@typescript-eslint/eslint-plugin": "5.0.0",
66 | "@typescript-eslint/parser": "5.0.0",
67 | "eslint": "8.0.1",
68 | "eslint-config-prettier": "8.3.0",
69 | "eslint-plugin-prettier": "4.0.0",
70 | "jest": "28.1.3",
71 | "prettier": "2.3.2",
72 | "source-map-support": "0.5.20",
73 | "supertest": "6.1.3",
74 | "ts-jest": "28.0.8",
75 | "ts-loader": "9.2.3",
76 | "ts-node": "10.9.1",
77 | "tsconfig-paths": "4.1.0",
78 | "typescript": "4.7.4"
79 | },
80 | "jest": {
81 | "moduleFileExtensions": [
82 | "js",
83 | "json",
84 | "ts"
85 | ],
86 | "rootDir": "src",
87 | "testRegex": ".*\\.spec\\.ts$",
88 | "transform": {
89 | ".+\\.(t|j)s$": "ts-jest"
90 | },
91 | "collectCoverageFrom": [
92 | "**/*.(t|j)s"
93 | ],
94 | "coverageDirectory": "../coverage",
95 | "testEnvironment": "node"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/polynote-backend/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | docker compose build && docker push asgarovfarhad/polynote-api:1.0.0
--------------------------------------------------------------------------------
/polynote-backend/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MiddlewareConsumer,
3 | Module,
4 | NestModule,
5 | RequestMethod,
6 | } from '@nestjs/common';
7 | import { ConfigModule } from '@nestjs/config';
8 | import { JwtModule } from '@nestjs/jwt';
9 | import { AuthMiddleware } from 'src/middlewares/AuthMiddleware';
10 | import { ListenerModule } from 'src/modules/listener/listener.module';
11 | import { NoteModule } from 'src/modules/note/note.module';
12 | import { UploadModule } from 'src/modules/upload/upload.module';
13 | import { UserModule } from 'src/modules/user/user.module';
14 | import { AiModule } from './modules/ai/ai.module';
15 |
16 | @Module({
17 | imports: [
18 | ConfigModule.forRoot({ isGlobal: true }),
19 | JwtModule.register({ secret: process.env.PRIVATE_KEY }),
20 |
21 | UserModule,
22 | NoteModule,
23 | ListenerModule,
24 | UploadModule,
25 | AiModule,
26 | ],
27 | })
28 | export class AppModule implements NestModule {
29 | configure(consumer: MiddlewareConsumer) {
30 | consumer
31 | .apply(AuthMiddleware)
32 | .exclude(
33 | {
34 | path: '/api/v1/user/auth',
35 | method: RequestMethod.POST,
36 | },
37 | {
38 | path: '/api/v1/user',
39 | method: RequestMethod.POST,
40 | },
41 | {
42 | path: '/api/v1/user/(.*)',
43 | method: RequestMethod.GET,
44 | },
45 | {
46 | path: '/api/v1/notes/shared/(.*)',
47 | method: RequestMethod.POST,
48 | },
49 | )
50 | .forRoutes({ path: '*', method: RequestMethod.ALL });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/polynote-backend/src/collections/note.collection.ts:
--------------------------------------------------------------------------------
1 | export const NoteCollection = `@public
2 | collection Note {
3 | id: string;
4 | publicKey: PublicKey;
5 | address: string;
6 | emoji: string;
7 | title: string;
8 | content: string;
9 | created: number;
10 | updated: number;
11 |
12 | constructor (id: string, address: string, emoji: string, title: string, content: string, created: number, updated: number) {
13 | this.id = id;
14 | this.address = address;
15 | this.emoji = emoji;
16 | this.title = title;
17 | this.content = content;
18 | this.created = created;
19 | this.updated = updated;
20 |
21 | this.publicKey = ctx.publicKey;
22 | }
23 |
24 | updateNote(title: string, emoji: string, content: string, updated: number) {
25 | if (this.publicKey != ctx.publicKey) {
26 | throw error('invalid public key');
27 | }
28 |
29 | this.title = title;
30 | this.emoji = emoji;
31 | this.content = content;
32 | this.updated = updated;
33 | }
34 |
35 | deleteNote() {
36 | if (this.publicKey != ctx.publicKey) {
37 | throw error('invalid public key');
38 | }
39 | selfdestruct();
40 | }
41 | }`;
42 |
--------------------------------------------------------------------------------
/polynote-backend/src/collections/user.collection.ts:
--------------------------------------------------------------------------------
1 | export const UserCollection = `@public
2 | collection User {
3 | id: string;
4 | publicKey: PublicKey;
5 | signature: string;
6 | address: string;
7 | name?: string;
8 |
9 | constructor (id: string, signature: string, address: string) {
10 | this.id = id;
11 | this.signature = signature;
12 | this.address = address;
13 | this.publicKey = ctx.publicKey;
14 | }
15 |
16 | updateName(name: string) {
17 | if (this.publicKey != ctx.publicKey) {
18 | throw error('invalid public key');
19 | }
20 | this.name = name;
21 | }
22 |
23 | updateSignature(signature: string) {
24 | if (this.publicKey != ctx.publicKey) {
25 | throw error('invalid public key');
26 | }
27 | this.signature = signature;
28 | }
29 |
30 | deleteUser() {
31 | if (this.publicKey != ctx.publicKey) {
32 | throw error('invalid public key');
33 | }
34 | selfdestruct();
35 | }
36 | }`;
37 |
--------------------------------------------------------------------------------
/polynote-backend/src/config.ts:
--------------------------------------------------------------------------------
1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm';
2 | import { config } from 'dotenv';
3 |
4 | config();
5 |
6 | export const CONFIG: Config = {
7 | MYSQL: {
8 | type: 'mysql',
9 | host: process.env.DB_HOST as string,
10 | port: Number(process.env.DB_PORT),
11 | username: process.env.DB_USER as string,
12 | password: process.env.DB_PASSWORD as string,
13 | database: process.env.DB_NAME as string,
14 | entities: [],
15 | synchronize: true,
16 | autoLoadEntities: true,
17 | } as TypeOrmModuleOptions,
18 | PORT: Number(process.env.APP_PORT),
19 | POLYNOTE_CONTRACT: process.env.POLYNOTE_CONTRACT,
20 | PUSH_CHANNEL_CAIP: `eip155:5:0xA63A810228a180767d3502EF8d21DbF4Da0D6b43`,
21 | APP_VERSION: process.env.APP_VERSION as 'development' | 'production',
22 | DB_NAMESPACE: process.env.DB_NAMESPACE,
23 | WEB3_STORAGE_TOKEN: process.env.WEB3_STORAGE_TOKEN,
24 | OPENAI_API_KEY: process.env.OPEN_AI_KEY,
25 | CLAVE_REGISTRY_ADDRESS: '0x9DCbb5f7454E1a9bE5f463415B03201EB7DAe69f',
26 | CLAVE_VALIDATOR_ADDRESS: '0x379f41Ab03B8e62A91aF1695fd70796ef51D4cfa',
27 | };
28 |
29 | type Config = {
30 | MYSQL: TypeOrmModuleOptions;
31 | PORT: number;
32 | POLYNOTE_CONTRACT: string;
33 | PUSH_CHANNEL_CAIP: string;
34 | APP_VERSION: 'development' | 'production';
35 | DB_NAMESPACE: string;
36 | WEB3_STORAGE_TOKEN: string;
37 | OPENAI_API_KEY: string;
38 | CLAVE_REGISTRY_ADDRESS: string;
39 | CLAVE_VALIDATOR_ADDRESS: string;
40 | };
41 |
--------------------------------------------------------------------------------
/polynote-backend/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import helmet from 'helmet';
3 | import { AppModule } from './app.module';
4 | import { CONFIG } from 'src/config';
5 | import { initPolybase } from 'src/utils/initPolybase';
6 | import { VersioningType } from '@nestjs/common';
7 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
8 |
9 | async function bootstrap() {
10 | const app = await NestFactory.create(AppModule);
11 |
12 | app.enableCors({
13 | origin: process.env.APP_CORS,
14 | });
15 | app.use(helmet());
16 |
17 | app.enableVersioning({
18 | type: VersioningType.URI,
19 | prefix: 'api/v',
20 | defaultVersion: process.env.API_VERSION,
21 | });
22 |
23 | const config = new DocumentBuilder()
24 | .setTitle('Polynote')
25 | .setDescription('The Polynote API Docs')
26 | .setVersion(process.env.APP_VERSION)
27 | .build();
28 | const document = SwaggerModule.createDocument(app, config);
29 | SwaggerModule.setup('docs', app, document);
30 |
31 | await initPolybase();
32 |
33 | await app.listen(CONFIG.PORT);
34 | }
35 | bootstrap();
36 |
--------------------------------------------------------------------------------
/polynote-backend/src/middlewares/AuthMiddleware.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HttpException,
3 | HttpStatus,
4 | Injectable,
5 | NestMiddleware,
6 | } from '@nestjs/common';
7 | import { JwtService } from '@nestjs/jwt';
8 | import { Request, Response, NextFunction } from 'express';
9 |
10 | @Injectable()
11 | export class AuthMiddleware implements NestMiddleware {
12 | constructor(private readonly jwtService: JwtService) {}
13 |
14 | use(req: Request, res: Response, next: NextFunction) {
15 | const token = req.headers.authorization;
16 |
17 | if (token == null) {
18 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
19 | }
20 | const parsed = req.headers.authorization.split('Bearer ')[1];
21 | try {
22 | const data = this.jwtService.verify(parsed);
23 | req.headers['address'] = data.address;
24 | next();
25 | } catch {
26 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/ai/ai.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post } from '@nestjs/common';
2 | import { AiService } from './ai.service';
3 | import { AITextDto } from './ai.dto';
4 | import { ApiTags } from '@nestjs/swagger';
5 |
6 | @ApiTags('AI')
7 | @Controller('ai')
8 | export class AiController {
9 | constructor(private aiService: AiService) {}
10 |
11 | @Post()
12 | public async makeLonger(@Body() aiTextDto: AITextDto) {
13 | if (aiTextDto.mode === 'fix-grammar') {
14 | const data = await this.aiService.fixGrammar(aiTextDto.text);
15 | return { text: data };
16 | } else if (aiTextDto.mode === 'make-longer') {
17 | const data = await this.aiService.makeLonger(aiTextDto.text);
18 | return { text: data };
19 | } else if (aiTextDto.mode === 'summarize') {
20 | const data = await this.aiService.summarize(aiTextDto.text);
21 | return { text: data };
22 | } else {
23 | return { text: '' };
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/ai/ai.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsNotEmpty, IsString } from 'class-validator';
3 |
4 | export class AITextDto {
5 | @ApiProperty()
6 | @IsNotEmpty()
7 | @IsString()
8 | text: string;
9 |
10 | @ApiProperty()
11 | @IsNotEmpty()
12 | @IsString()
13 | mode: 'make-longer' | 'summarize' | 'fix-grammar';
14 | }
15 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/ai/ai.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AiController } from './ai.controller';
3 | import { AiService } from './ai.service';
4 |
5 | @Module({
6 | controllers: [AiController],
7 | providers: [AiService],
8 | })
9 | export class AiModule {}
10 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/ai/ai.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import OpenAI from 'openai';
3 | import { CONFIG } from 'src/config';
4 |
5 | @Injectable()
6 | export class AiService {
7 | openai: OpenAI;
8 |
9 | constructor() {
10 | this.openai = new OpenAI({
11 | apiKey: CONFIG.OPENAI_API_KEY, // This is also the default, can be omitted
12 | });
13 | }
14 |
15 | public async makeLonger(text: string) {
16 | const res = await this.openai.chat.completions.create({
17 | model: 'gpt-3.5-turbo',
18 | messages: [
19 | {
20 | role: 'user',
21 | content: `Make the following content longer:\n ${text}`,
22 | },
23 | ],
24 | });
25 |
26 | return (
27 | res.choices[0].message.content ?? 'I could not make the text longer.'
28 | );
29 | }
30 |
31 | public async summarize(text: string) {
32 | const res = await this.openai.chat.completions.create({
33 | model: 'gpt-3.5-turbo',
34 | messages: [
35 | {
36 | role: 'user',
37 | content: `Summarize the following content:\n ${text}`,
38 | },
39 | ],
40 | });
41 |
42 | return res.choices[0].message?.content ?? 'I could not summarize the text.';
43 | }
44 |
45 | public async fixGrammar(text: string) {
46 | const res = await this.openai.chat.completions.create({
47 | model: 'gpt-3.5-turbo',
48 | messages: [
49 | {
50 | role: 'user',
51 | content: `Fix the grammar of the following content:\n ${text}`,
52 | },
53 | ],
54 | });
55 |
56 | return (
57 | res.choices[0].message?.content ??
58 | "I'm sorry, I couldn't fix the grammar of the given text."
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/listener/listener.controller.ts:
--------------------------------------------------------------------------------
1 | import { JsonRpcProvider } from '@ethersproject/providers';
2 | import { Controller, OnApplicationBootstrap } from '@nestjs/common';
3 | import { ethers } from 'ethers';
4 | import { CONFIG } from 'src/config';
5 | import { POLYNOTE_ABI } from 'src/utils/abi';
6 | import { getRpcProvider } from 'src/utils/getRpcProvider';
7 | import * as PushAPI from '@pushprotocol/restapi';
8 | import { ENV } from '@pushprotocol/restapi/src/lib/constants';
9 | import { formatAddress } from 'src/utils/formatAddress';
10 |
11 | @Controller('listener')
12 | export class ListenerController implements OnApplicationBootstrap {
13 | provider: JsonRpcProvider;
14 | constructor() {
15 | this.provider = getRpcProvider();
16 | }
17 |
18 | public async onApplicationBootstrap() {
19 | const contract = new ethers.Contract(
20 | CONFIG.POLYNOTE_CONTRACT,
21 | POLYNOTE_ABI,
22 | this.provider,
23 | );
24 |
25 | contract.on(
26 | 'Shared',
27 | async (sender: string, noteId: string, recipient: string) => {
28 | await this.sendNotification(sender, noteId, recipient);
29 | },
30 | );
31 | }
32 |
33 | async sendNotification(sender: string, noteId: string, recipient: string) {
34 | if (CONFIG.APP_VERSION === 'development') {
35 | return;
36 | }
37 |
38 | const PK = process.env.PRIVATE_KEY; // channel private key
39 | const _signer = new ethers.Wallet(PK);
40 |
41 | try {
42 | await PushAPI.payloads.sendNotification({
43 | signer: _signer,
44 | type: 1,
45 | identityType: 2,
46 | notification: {
47 | title: `Polynote - Share`,
48 | body: `${formatAddress(
49 | sender,
50 | )} gave you an access to see the note with an ID of ${noteId}`,
51 | },
52 | payload: {
53 | title: `Polynote - Share`,
54 | body: `${formatAddress(
55 | sender,
56 | )} gave you an access to see the note with an ID of ${noteId}`,
57 | cta: recipient,
58 | img: '',
59 | },
60 | recipients: [recipient],
61 | channel: CONFIG.PUSH_CHANNEL_CAIP,
62 | env: ENV.STAGING,
63 | });
64 | } catch (err) {
65 | console.log(err?.response);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/listener/listener.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ListenerController } from 'src/modules/listener/listener.controller';
3 |
4 | @Module({
5 | controllers: [ListenerController],
6 | providers: [],
7 | imports: [],
8 | })
9 | export class ListenerModule {}
10 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/note/note.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsNotEmpty, IsString } from 'class-validator';
3 |
4 | export class NotesParams {
5 | @ApiProperty()
6 | @IsNotEmpty()
7 | @IsString()
8 | address: string;
9 | }
10 |
11 | export class NotesIdParam {
12 | @ApiProperty()
13 | @IsNotEmpty()
14 | @IsString()
15 | id: string;
16 | }
17 |
18 | export class NotesSharedParam {
19 | @ApiProperty()
20 | @IsNotEmpty()
21 | @IsString()
22 | address: string;
23 |
24 | @ApiProperty()
25 | @IsNotEmpty()
26 | @IsString()
27 | signature: string;
28 | }
29 |
30 | export class NotesUpdateParams {
31 | @ApiProperty()
32 | @IsNotEmpty()
33 | @IsString()
34 | id: string;
35 | }
36 |
37 | export class NotesCreateDto {
38 | @ApiProperty()
39 | @IsNotEmpty()
40 | @IsString()
41 | address: string;
42 |
43 | @ApiProperty()
44 | @IsNotEmpty()
45 | @IsString()
46 | emoji: string;
47 |
48 | @ApiProperty()
49 | @IsNotEmpty()
50 | @IsString()
51 | content: string;
52 |
53 | @ApiProperty()
54 | @IsNotEmpty()
55 | @IsString()
56 | title: string;
57 | }
58 |
59 | export class NotesUpdateDto {
60 | @ApiProperty()
61 | @IsNotEmpty()
62 | @IsString()
63 | emoji: string;
64 |
65 | @ApiProperty()
66 | @IsNotEmpty()
67 | @IsString()
68 | content: string;
69 |
70 | @ApiProperty()
71 | @IsNotEmpty()
72 | @IsString()
73 | title: string;
74 | }
75 |
76 | export class NotesResponseData {
77 | @ApiProperty()
78 | @IsNotEmpty()
79 | @IsString()
80 | id: string;
81 |
82 | @ApiProperty()
83 | @IsNotEmpty()
84 | @IsString()
85 | address: string;
86 |
87 | @ApiProperty()
88 | @IsNotEmpty()
89 | @IsString()
90 | emoji: string;
91 |
92 | @ApiProperty()
93 | @IsNotEmpty()
94 | @IsString()
95 | content: string;
96 |
97 | @ApiProperty()
98 | @IsNotEmpty()
99 | @IsString()
100 | title: string;
101 | }
102 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/note/note.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { NoteController } from 'src/modules/note/note.controller';
3 | import { NoteService } from 'src/modules/note/note.service';
4 |
5 | @Module({
6 | controllers: [NoteController],
7 | providers: [NoteService],
8 | imports: [],
9 | })
10 | export class NoteModule {}
11 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/note/note.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Collection, Polybase } from '@polybase/client';
3 | import {
4 | NotesCreateDto,
5 | NotesResponseData,
6 | NotesUpdateDto,
7 | } from 'src/modules/note/note.dto';
8 | import { getPolybaseInstance } from 'src/utils/getPolybaseInstance';
9 | import { getTimestamp } from 'src/utils/getTimestamp';
10 | import { v4 as uuidv4 } from 'uuid';
11 | import {
12 | aescbc,
13 | decodeFromString,
14 | encodeToString,
15 | EncryptedDataAesCbc256,
16 | } from '@polybase/util';
17 |
18 | const getEncryptionKey = () => {
19 | const key = process.env.ENCRYPTION_KEY;
20 | return new Uint8Array(JSON.parse(key));
21 | };
22 |
23 | @Injectable()
24 | export class NoteService {
25 | db: Polybase;
26 | collection: Collection;
27 |
28 | constructor() {
29 | this.db = getPolybaseInstance();
30 | this.collection = this.db.collection('Note');
31 | }
32 |
33 | public async genNotesForUser(address: string): Promise {
34 | const response = await this.collection
35 | .where('address', '==', address)
36 | .get();
37 |
38 | const decryptedContentValues: string[] = [];
39 |
40 | for (const item of response.data) {
41 | try {
42 | const decrypted = await this.decryptString(item.data.content);
43 | decryptedContentValues.push(decrypted);
44 | } catch (error) {
45 | decryptedContentValues.push(item.data.content);
46 | }
47 | }
48 |
49 | return response.data.map((item, index) => {
50 | return {
51 | ...item.data,
52 | content: decryptedContentValues[index],
53 | };
54 | });
55 | }
56 |
57 | public async genNoteById(id: string): Promise {
58 | const response = await this.collection.record(id).get();
59 | return response.data;
60 | }
61 |
62 | public async genDecryptedNoteById(id: string): Promise {
63 | const response = await this.collection.record(id).get();
64 |
65 | const decryptedContent = await this.decryptString(response.data.content);
66 |
67 | return { ...response.data, content: decryptedContent };
68 | }
69 |
70 | public async updateNote(
71 | id: string,
72 | noteUpdateDto: NotesUpdateDto,
73 | ): Promise {
74 | const note = await this.genNoteById(id);
75 | const encryptedData = await this.encyptString(noteUpdateDto.content);
76 |
77 | if (
78 | encryptedData === note.content &&
79 | noteUpdateDto.title === note.title &&
80 | noteUpdateDto.emoji === note.emoji
81 | ) {
82 | return note;
83 | }
84 |
85 | const response = await this.collection
86 | .record(id)
87 | .call('updateNote', [
88 | noteUpdateDto.title,
89 | noteUpdateDto.emoji,
90 | encryptedData,
91 | getTimestamp(),
92 | ]);
93 |
94 | const decrpted = await this.decryptString(response.data.content);
95 | delete response.data['content'];
96 |
97 | return { ...response.data, content: decrpted };
98 | }
99 |
100 | public async createNote(notesCreateDto: NotesCreateDto) {
101 | const id = uuidv4();
102 |
103 | const encryptedContent = await this.encyptString(notesCreateDto.content);
104 |
105 | await this.collection.create([
106 | id,
107 | notesCreateDto.address,
108 | notesCreateDto.emoji,
109 | notesCreateDto.title,
110 | encryptedContent,
111 | getTimestamp(),
112 | getTimestamp(),
113 | ]);
114 |
115 | return await this.genDecryptedNoteById(id);
116 | }
117 |
118 | public async deleteNoteById(id: string) {
119 | return await this.collection.record(id).call('deleteNote');
120 | }
121 |
122 | public async encyptString(data: string): Promise {
123 | const strDataToBeEncrypted = decodeFromString(data, 'utf8');
124 |
125 | const encryptedData = await aescbc.symmetricEncrypt(
126 | getEncryptionKey(),
127 | strDataToBeEncrypted,
128 | );
129 |
130 | return JSON.stringify(encryptedData);
131 | }
132 |
133 | public async decryptString(data: string): Promise {
134 | const parsed = JSON.parse(data);
135 |
136 | const actual: EncryptedDataAesCbc256 = {
137 | version: parsed.version,
138 | nonce: new Uint8Array(
139 | Object.keys(parsed.nonce).map((item) => parsed.nonce[item]),
140 | ),
141 | ciphertext: new Uint8Array(
142 | Object.keys(parsed.ciphertext).map((item) => parsed.ciphertext[item]),
143 | ),
144 | };
145 |
146 | const decrypted = await aescbc.symmetricDecrypt(getEncryptionKey(), actual);
147 | return encodeToString(decrypted, 'utf8');
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/upload/upload.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | UploadedFile,
5 | UseInterceptors,
6 | } from '@nestjs/common';
7 | import { FileInterceptor } from '@nestjs/platform-express';
8 | import { ApiTags } from '@nestjs/swagger';
9 | import { UploadService } from 'src/modules/upload/upload.service';
10 |
11 | @ApiTags('Upload')
12 | @Controller('upload')
13 | export class UploadController {
14 | constructor(private uploadService: UploadService) {}
15 |
16 | @Post()
17 | @UseInterceptors(FileInterceptor('file'))
18 | public async uploadFile(@UploadedFile() file: Express.Multer.File) {
19 | const url = await this.uploadService.uploadImage(file);
20 |
21 | return {
22 | url,
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/upload/upload.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UploadController } from 'src/modules/upload/upload.controller';
3 | import { UploadService } from 'src/modules/upload/upload.service';
4 |
5 | @Module({
6 | controllers: [UploadController],
7 | providers: [UploadService],
8 | imports: [],
9 | })
10 | export class UploadModule {}
11 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/upload/upload.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2 | import { CONFIG } from 'src/config';
3 | import { MB } from 'src/utils/numbers';
4 | import { Blob, File, Web3Storage } from 'web3.storage';
5 |
6 | @Injectable()
7 | export class UploadService {
8 | client: Web3Storage;
9 | rootURL: string;
10 |
11 | constructor() {
12 | this.client = new Web3Storage({
13 | token: CONFIG.WEB3_STORAGE_TOKEN,
14 | });
15 | this.rootURL = 'https://dweb.link/ipfs';
16 | }
17 |
18 | public async uploadImage(file: Express.Multer.File) {
19 | const allowedFileTypes = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
20 |
21 | const fileSizeInMB = file.size / MB;
22 | if (fileSizeInMB > 2) {
23 | throw new HttpException(
24 | 'File size cannot be larger than 2MB',
25 | HttpStatus.BAD_REQUEST,
26 | );
27 | }
28 |
29 | const filename = file.originalname || file.filename;
30 | const extension = filename.split('.').pop();
31 |
32 | if (!allowedFileTypes.includes(extension)) {
33 | throw new HttpException(
34 | 'You can only upload an image',
35 | HttpStatus.BAD_REQUEST,
36 | );
37 | }
38 |
39 | const blob = new Blob([file.buffer]);
40 | const _file = new File([blob], filename);
41 | const cid = await this.client.put([_file]);
42 | const url = this.formatWeb3StorageUrl(cid, filename);
43 | return url;
44 | }
45 |
46 | formatWeb3StorageUrl(cid: number | string, fileName: string) {
47 | return `${this.rootURL}/${cid}/${fileName}`;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpException,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | Put,
11 | Req,
12 | UsePipes,
13 | ValidationPipe,
14 | } from '@nestjs/common';
15 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
16 | import { Request } from 'express';
17 | import {
18 | UserAddressDto,
19 | UserAuthDto,
20 | UserCreateDto,
21 | UserDeleteDto,
22 | UserResponse,
23 | UserResponseDto,
24 | UserTokenResponse,
25 | UserUpdateDto,
26 | } from 'src/modules/user/user.dto';
27 | import { UserService } from 'src/modules/user/user.service';
28 | import { checkAdmin } from 'src/utils/checkAdmin';
29 | import { getTokenData } from 'src/utils/getTokenData';
30 |
31 | @ApiTags('User')
32 | @Controller('user')
33 | export class UserController {
34 | constructor(private userService: UserService) {}
35 |
36 | @Get()
37 | @ApiResponse({
38 | type: UserResponseDto,
39 | isArray: true,
40 | })
41 | @ApiOperation({ summary: 'Get all users' })
42 | public async genUsers(): Promise {
43 | const result = await this.userService.genUsers();
44 | return result;
45 | }
46 |
47 | @Get('/:address')
48 | @ApiOperation({ summary: 'Get user with an address' })
49 | @ApiResponse({
50 | type: UserResponse,
51 | })
52 | public async genUserByAddress(@Param() userAddressDto: UserAddressDto) {
53 | const result = await this.userService.genUserByAddress(
54 | userAddressDto.address,
55 | );
56 | return result;
57 | }
58 |
59 | @Post()
60 | @ApiOperation({ summary: 'Create new user' })
61 | @ApiResponse({
62 | type: UserResponseDto,
63 | })
64 | @UsePipes(new ValidationPipe())
65 | public async createUser(@Body() userCreateDto: UserCreateDto) {
66 | return await this.userService.createUser(userCreateDto);
67 | }
68 |
69 | @Post('/auth')
70 | @ApiOperation({ summary: 'Authorize user with signature' })
71 | @ApiResponse({
72 | type: UserTokenResponse,
73 | })
74 | @UsePipes(new ValidationPipe())
75 | public async authUser(@Body() userAuthDto: UserAuthDto) {
76 | return await this.userService.authUser(userAuthDto);
77 | }
78 |
79 | @Put('/:address')
80 | @ApiOperation({ summary: 'Update user name' })
81 | @ApiResponse({
82 | type: UserResponseDto,
83 | })
84 | @UsePipes(new ValidationPipe())
85 | public async updateUserName(
86 | @Param() userAddressDto: UserAddressDto,
87 | @Body() userUpdateDto: UserUpdateDto,
88 | @Req() req: Request,
89 | ) {
90 | const { address } = getTokenData(req);
91 |
92 | if (userAddressDto.address !== address) {
93 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
94 | }
95 |
96 | return await this.userService.updateUserName(
97 | userAddressDto.address,
98 | userUpdateDto.name,
99 | );
100 | }
101 |
102 | @Delete('/:address')
103 | @ApiOperation({ summary: 'Delete user with address (only admin)' })
104 | @UsePipes(new ValidationPipe())
105 | public async deleteUser(
106 | @Param() userAddressDto: UserDeleteDto,
107 | @Req() req: Request,
108 | ) {
109 | const { address } = getTokenData(req);
110 |
111 | if (!checkAdmin(address)) {
112 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
113 | }
114 |
115 | return await this.userService.deleteUser(userAddressDto.address);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/user/user.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { PublicKey } from '@polybase/client';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | export class UserCreateDto {
6 | @ApiProperty()
7 | @IsNotEmpty()
8 | @IsString()
9 | address: string;
10 |
11 | @ApiProperty()
12 | @IsNotEmpty()
13 | @IsString()
14 | signature: string;
15 | }
16 |
17 | export class UserAuthDto {
18 | @ApiProperty()
19 | @IsNotEmpty()
20 | @IsString()
21 | address: string;
22 |
23 | @ApiProperty()
24 | @IsNotEmpty()
25 | @IsString()
26 | signature: string;
27 | }
28 |
29 | export class UserUpdateDto {
30 | @ApiProperty()
31 | @IsNotEmpty()
32 | @IsString()
33 | name: string;
34 | }
35 |
36 | export class UserPushNotificationDto {
37 | @ApiProperty()
38 | @IsNotEmpty()
39 | @IsString()
40 | address: string;
41 | }
42 |
43 | export class UserDeleteDto {
44 | @ApiProperty()
45 | @IsNotEmpty()
46 | @IsString()
47 | address: string;
48 | }
49 |
50 | export class UserAddressDto {
51 | @ApiProperty()
52 | @IsNotEmpty()
53 | @IsString()
54 | address: string;
55 | }
56 |
57 | export class UserResponseDto {
58 | @ApiProperty()
59 | address: string;
60 |
61 | @ApiProperty()
62 | publicKey: PublicKey;
63 |
64 | @ApiProperty()
65 | id: string;
66 |
67 | @ApiProperty()
68 | name: string;
69 | }
70 |
71 | export class UserResponse {
72 | @ApiProperty({ nullable: true })
73 | user: UserResponseDto | null;
74 | }
75 |
76 | export class UserTokenResponse {
77 | @ApiProperty()
78 | token: string;
79 | }
80 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { JwtModule } from '@nestjs/jwt';
3 | import { UserController } from 'src/modules/user/user.controller';
4 | import { UserService } from 'src/modules/user/user.service';
5 |
6 | @Module({
7 | controllers: [UserController],
8 | providers: [UserService],
9 | imports: [JwtModule.register({ secret: process.env.PRIVATE_KEY })],
10 | })
11 | export class UserModule {}
12 |
--------------------------------------------------------------------------------
/polynote-backend/src/modules/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { Collection, Polybase } from '@polybase/client';
4 | import { ethers } from 'ethers';
5 | import {
6 | UserAuthDto,
7 | UserCreateDto,
8 | UserResponseDto,
9 | } from 'src/modules/user/user.dto';
10 | import { getPolybaseInstance } from 'src/utils/getPolybaseInstance';
11 | import { v4 as uuidv4 } from 'uuid';
12 | import * as PushAPI from '@pushprotocol/restapi';
13 | import { CONFIG } from 'src/config';
14 | import { ENV } from '@pushprotocol/restapi/src/lib/constants';
15 | import { Messages } from 'src/utils';
16 | import { verifySignature } from 'src/utils/verifySignature';
17 |
18 | @Injectable()
19 | export class UserService {
20 | db: Polybase;
21 | collection: Collection;
22 |
23 | constructor(private readonly jwtService: JwtService) {
24 | this.db = getPolybaseInstance();
25 | this.collection = this.db.collection('User');
26 | }
27 |
28 | public async genUsers(): Promise {
29 | const response = await this.collection.get();
30 |
31 | return response.data.map((item) => {
32 | return item.data;
33 | });
34 | }
35 |
36 | public async genUserByAddress(
37 | address: string,
38 | ): Promise<{ user: UserResponseDto | null }> {
39 | const response = await this.collection
40 | .where('address', '==', address)
41 | .get();
42 |
43 | if (response == null) {
44 | return { user: null };
45 | }
46 |
47 | if (Array.isArray(response.data)) {
48 | return { user: (response.data[0]?.data as any) ?? null };
49 | }
50 |
51 | return { user: null };
52 | }
53 |
54 | public async updateUserName(address: string, name: string) {
55 | const { user } = await this.genUserByAddress(address);
56 |
57 | if (user == null) {
58 | throw new HttpException('User does not exist', HttpStatus.NOT_FOUND);
59 | }
60 |
61 | await this.collection
62 | .record(user.id)
63 | .call('updateName', [name.slice(0, 64)]);
64 |
65 | return await this.genUserByAddress(address);
66 | }
67 |
68 | public async createUser(userCreateDto: UserCreateDto) {
69 | const verification = await verifySignature(
70 | Messages.CREATE_ACCOUNT,
71 | userCreateDto.signature,
72 | userCreateDto.address,
73 | );
74 |
75 | if (!verification) {
76 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
77 | }
78 |
79 | const response = await this.collection.create([
80 | uuidv4(),
81 | userCreateDto.signature,
82 | userCreateDto.address,
83 | ]);
84 |
85 | return response.data;
86 | }
87 |
88 | public async authUser(userAuthDto: UserAuthDto) {
89 | const verification = await verifySignature(
90 | Messages.SIGN_IN,
91 | userAuthDto.signature,
92 | userAuthDto.address,
93 | );
94 |
95 | if (verification) {
96 | const token = this.jwtService.sign({ address: userAuthDto.address });
97 |
98 | return { token };
99 | }
100 |
101 | throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
102 | }
103 |
104 | public async deleteUser(address: string) {
105 | const { user } = await this.genUserByAddress(address);
106 |
107 | if (user == null) {
108 | throw new HttpException('User does not exist', HttpStatus.NOT_FOUND);
109 | }
110 |
111 | await this.collection.record(user.id).call('deleteUser');
112 | }
113 |
114 | public async optIn(address: string) {
115 | const PK = process.env.PRIVATE_KEY; // channel private key
116 | const _signer = new ethers.Wallet(PK);
117 | await PushAPI.channels.subscribe({
118 | signer: _signer,
119 | channelAddress: CONFIG.PUSH_CHANNEL_CAIP,
120 | userAddress: `eip155:5:${address}`,
121 | env: ENV.STAGING,
122 | });
123 |
124 | return { status: 'ok' };
125 | }
126 |
127 | public async optOut(address: string) {
128 | const PK = process.env.PRIVATE_KEY; // channel private key
129 | const _signer = new ethers.Wallet(PK);
130 | await PushAPI.channels.unsubscribe({
131 | signer: _signer,
132 | channelAddress: CONFIG.PUSH_CHANNEL_CAIP,
133 | userAddress: `eip155:5:${address}`,
134 | env: ENV.STAGING,
135 | });
136 | return { status: 'ok' };
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/abi.ts:
--------------------------------------------------------------------------------
1 | export const POLYNOTE_ABI = [
2 | {
3 | anonymous: false,
4 | inputs: [
5 | {
6 | indexed: false,
7 | internalType: 'address',
8 | name: 'owner',
9 | type: 'address',
10 | },
11 | {
12 | indexed: false,
13 | internalType: 'string',
14 | name: 'noteId',
15 | type: 'string',
16 | },
17 | {
18 | indexed: false,
19 | internalType: 'address',
20 | name: 'partner',
21 | type: 'address',
22 | },
23 | ],
24 | name: 'Shared',
25 | type: 'event',
26 | },
27 | {
28 | anonymous: false,
29 | inputs: [
30 | {
31 | indexed: false,
32 | internalType: 'address',
33 | name: 'owner',
34 | type: 'address',
35 | },
36 | {
37 | indexed: false,
38 | internalType: 'string',
39 | name: 'noteId',
40 | type: 'string',
41 | },
42 | {
43 | indexed: false,
44 | internalType: 'address',
45 | name: 'partner',
46 | type: 'address',
47 | },
48 | ],
49 | name: 'Unshared',
50 | type: 'event',
51 | },
52 | {
53 | inputs: [
54 | {
55 | internalType: 'string',
56 | name: '_noteId',
57 | type: 'string',
58 | },
59 | {
60 | internalType: 'address[]',
61 | name: '_partners',
62 | type: 'address[]',
63 | },
64 | ],
65 | name: 'addPartners',
66 | outputs: [],
67 | stateMutability: 'nonpayable',
68 | type: 'function',
69 | },
70 | {
71 | inputs: [
72 | {
73 | internalType: 'address',
74 | name: '_owner',
75 | type: 'address',
76 | },
77 | {
78 | internalType: 'string',
79 | name: '_noteId',
80 | type: 'string',
81 | },
82 | ],
83 | name: 'getSharedAddresses',
84 | outputs: [
85 | {
86 | internalType: 'address[]',
87 | name: '',
88 | type: 'address[]',
89 | },
90 | ],
91 | stateMutability: 'view',
92 | type: 'function',
93 | },
94 | {
95 | inputs: [
96 | {
97 | internalType: 'address',
98 | name: '_owner',
99 | type: 'address',
100 | },
101 | {
102 | internalType: 'string',
103 | name: '_noteId',
104 | type: 'string',
105 | },
106 | {
107 | internalType: 'address',
108 | name: '_partner',
109 | type: 'address',
110 | },
111 | ],
112 | name: 'isShared',
113 | outputs: [
114 | {
115 | internalType: 'bool',
116 | name: '',
117 | type: 'bool',
118 | },
119 | ],
120 | stateMutability: 'view',
121 | type: 'function',
122 | },
123 | {
124 | inputs: [
125 | {
126 | internalType: 'string',
127 | name: '_noteId',
128 | type: 'string',
129 | },
130 | {
131 | internalType: 'address[]',
132 | name: '_partners',
133 | type: 'address[]',
134 | },
135 | ],
136 | name: 'removePartners',
137 | outputs: [],
138 | stateMutability: 'nonpayable',
139 | type: 'function',
140 | },
141 | {
142 | inputs: [
143 | {
144 | internalType: 'string',
145 | name: '_noteId',
146 | type: 'string',
147 | },
148 | {
149 | internalType: 'address[]',
150 | name: '_partners',
151 | type: 'address[]',
152 | },
153 | ],
154 | name: 'setPartners',
155 | outputs: [],
156 | stateMutability: 'nonpayable',
157 | type: 'function',
158 | },
159 | ];
160 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/checkAdmin.ts:
--------------------------------------------------------------------------------
1 | export const checkAdmin = (address: string) => {
2 | return (
3 | address.toLowerCase() ===
4 | '0xd0c3386D693A303f66cE76C79CD1549DFB5F1e0D'.toLowerCase()
5 | );
6 | };
7 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/formatAddress.ts:
--------------------------------------------------------------------------------
1 | export const formatAddress = (address: string, pad = 5) => {
2 | return (
3 | address?.substring?.(0, pad) +
4 | '...' +
5 | address?.substring?.(address?.length - pad)
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/getPolybaseInstance.ts:
--------------------------------------------------------------------------------
1 | import { db } from 'src/utils/initPolybase';
2 |
3 | export const getPolybaseInstance = () => {
4 | return db;
5 | };
6 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/getRpcProvider.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export class JsonRpcProvider extends ethers.providers.JsonRpcProvider {}
4 |
5 | export type ProviderEventFilter = {
6 | address: string;
7 | topics: Array;
8 | };
9 |
10 | export const getRpcProvider = (): JsonRpcProvider => {
11 | const provider = new JsonRpcProvider(process.env.NETWORK_RPC_URL);
12 | return provider;
13 | };
14 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/getTimestamp.ts:
--------------------------------------------------------------------------------
1 | export const getTimestamp = () => {
2 | return Math.floor(new Date().getTime() / 1000);
3 | };
4 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/getTokenData.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 |
3 | type TokenData = { address: string };
4 |
5 | export const getTokenData = (req: Request): TokenData => {
6 | return { address: req.headers.address as string };
7 | };
8 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { Messages } from './messages';
2 | export { verifyK1Signature } from './verifyK1Signature';
3 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/initPolybase.ts:
--------------------------------------------------------------------------------
1 | import { Polybase } from '@polybase/client';
2 | import { ethPersonalSign } from '@polybase/eth';
3 | import { NoteCollection } from 'src/collections/note.collection';
4 | import { UserCollection } from 'src/collections/user.collection';
5 | import { CONFIG } from 'src/config';
6 |
7 | export const db = new Polybase({
8 | signer: (data: any) => {
9 | return {
10 | h: 'eth-personal-sign',
11 | sig: ethPersonalSign(process.env.PRIVATE_KEY, data),
12 | };
13 | },
14 | defaultNamespace: CONFIG.DB_NAMESPACE,
15 | });
16 |
17 | export const initPolybase = async () => {
18 | try {
19 | await db.applySchema(UserCollection);
20 | } catch {}
21 |
22 | try {
23 | await db.applySchema(NoteCollection);
24 | } catch (err) {
25 | console.log(err);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/messages.ts:
--------------------------------------------------------------------------------
1 | export enum Messages {
2 | SIGN_IN = 'Polynote - Sign in',
3 | CREATE_ACCOUNT = 'Polynote - Create account',
4 | SEE_SHARED_NOTE = 'Polynote - See shared note',
5 | }
6 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/numbers.ts:
--------------------------------------------------------------------------------
1 | export const BYTES = 1024;
2 | export const MB = BYTES * 1024;
3 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/signature.ts:
--------------------------------------------------------------------------------
1 | import { CONFIG } from 'src/config';
2 |
3 | export const DOMAIN = {
4 | name: 'Polynote',
5 | version: '1',
6 | verifyingContract: CONFIG.POLYNOTE_CONTRACT,
7 | } as const;
8 |
9 | // The named list of all type definitions
10 | export const TYPES = {
11 | User: [
12 | { name: 'address', type: 'address' },
13 | { name: 'message', type: 'string' },
14 | ],
15 | };
16 |
17 | export const getSignatureValue = (address: `0x${string}`, message: string) => {
18 | return {
19 | address,
20 | message,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/verifyK1Signature.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | export const verifyK1Signature = async (
4 | message: string,
5 | signature: string,
6 | publicAddress: string,
7 | ): Promise => {
8 | const signer = ethers.utils.verifyMessage(message, signature);
9 |
10 | return signer.toLowerCase() === publicAddress.toLowerCase();
11 | };
12 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/verifyR1Signature.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { CONFIG } from 'src/config';
3 | import { Contract, Provider } from 'zksync-web3';
4 |
5 | const CLAVE_ACCOUNT_ABI = [
6 | {
7 | inputs: [
8 | {
9 | internalType: 'bytes32',
10 | name: 'signedHash',
11 | type: 'bytes32',
12 | },
13 | {
14 | internalType: 'bytes',
15 | name: 'signatureAndValidator',
16 | type: 'bytes',
17 | },
18 | ],
19 | name: 'isValidSignature',
20 | outputs: [
21 | {
22 | internalType: 'bytes4',
23 | name: 'magicValue',
24 | type: 'bytes4',
25 | },
26 | ],
27 | stateMutability: 'view',
28 | type: 'function',
29 | },
30 | ];
31 |
32 | export const CLAVE_REGISTRY_ABI = [
33 | {
34 | inputs: [],
35 | stateMutability: 'nonpayable',
36 | type: 'constructor',
37 | },
38 | {
39 | inputs: [],
40 | name: 'NOT_FROM_FACTORY',
41 | type: 'error',
42 | },
43 | {
44 | anonymous: false,
45 | inputs: [
46 | {
47 | indexed: true,
48 | internalType: 'address',
49 | name: 'previousOwner',
50 | type: 'address',
51 | },
52 | {
53 | indexed: true,
54 | internalType: 'address',
55 | name: 'newOwner',
56 | type: 'address',
57 | },
58 | ],
59 | name: 'OwnershipTransferred',
60 | type: 'event',
61 | },
62 | {
63 | inputs: [
64 | {
65 | internalType: 'address',
66 | name: '',
67 | type: 'address',
68 | },
69 | ],
70 | name: 'isClave',
71 | outputs: [
72 | {
73 | internalType: 'bool',
74 | name: '',
75 | type: 'bool',
76 | },
77 | ],
78 | stateMutability: 'view',
79 | type: 'function',
80 | },
81 | {
82 | inputs: [],
83 | name: 'owner',
84 | outputs: [
85 | {
86 | internalType: 'address',
87 | name: '',
88 | type: 'address',
89 | },
90 | ],
91 | stateMutability: 'view',
92 | type: 'function',
93 | },
94 | {
95 | inputs: [
96 | {
97 | internalType: 'address',
98 | name: 'account',
99 | type: 'address',
100 | },
101 | ],
102 | name: 'register',
103 | outputs: [],
104 | stateMutability: 'nonpayable',
105 | type: 'function',
106 | },
107 | {
108 | inputs: [],
109 | name: 'renounceOwnership',
110 | outputs: [],
111 | stateMutability: 'nonpayable',
112 | type: 'function',
113 | },
114 | {
115 | inputs: [
116 | {
117 | internalType: 'address',
118 | name: 'factory_',
119 | type: 'address',
120 | },
121 | ],
122 | name: 'setFactory',
123 | outputs: [],
124 | stateMutability: 'nonpayable',
125 | type: 'function',
126 | },
127 | {
128 | inputs: [
129 | {
130 | internalType: 'address',
131 | name: 'newOwner',
132 | type: 'address',
133 | },
134 | ],
135 | name: 'transferOwnership',
136 | outputs: [],
137 | stateMutability: 'nonpayable',
138 | type: 'function',
139 | },
140 | ];
141 |
142 | const formatHex = (hex: string) => (hex.includes('0x') ? hex : `0x${hex}`);
143 |
144 | export const verifyR1Signature = async (
145 | message: string,
146 | signature: string,
147 | publicAddress: string,
148 | ): Promise => {
149 | const provider = new Provider(process.env.NETWORK_RPC_URL);
150 |
151 | const signatureAndValidator = ethers.utils.defaultAbiCoder.encode(
152 | ['bytes', 'address'],
153 | [formatHex(signature), CONFIG.CLAVE_VALIDATOR_ADDRESS],
154 | );
155 | const signedHash = ethers.utils.sha256(Buffer.from(message));
156 |
157 | const contract = new Contract(publicAddress, CLAVE_ACCOUNT_ABI, provider);
158 |
159 | const isValidSignature = await contract.isValidSignature(
160 | signedHash,
161 | formatHex(signatureAndValidator),
162 | );
163 |
164 | return isValidSignature;
165 | };
166 |
--------------------------------------------------------------------------------
/polynote-backend/src/utils/verifySignature.ts:
--------------------------------------------------------------------------------
1 | import { CONFIG } from 'src/config';
2 | import { getRpcProvider } from 'src/utils/getRpcProvider';
3 | import { verifyK1Signature } from 'src/utils/verifyK1Signature';
4 | import {
5 | CLAVE_REGISTRY_ABI,
6 | verifyR1Signature,
7 | } from 'src/utils/verifyR1Signature';
8 | import { Contract } from 'zksync-web3';
9 |
10 | export const verifySignature = async (
11 | message: string,
12 | signature: string,
13 | publicAddress: string,
14 | ): Promise => {
15 | const provider = getRpcProvider();
16 |
17 | const registryContract = new Contract(
18 | CONFIG.CLAVE_REGISTRY_ADDRESS,
19 | CLAVE_REGISTRY_ABI,
20 | provider,
21 | );
22 |
23 | const isClave = await registryContract.isClave(publicAddress);
24 |
25 | if (isClave) {
26 | return await verifyR1Signature(message, signature, publicAddress);
27 | } else {
28 | return await verifyK1Signature(message, signature, publicAddress);
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/polynote-backend/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/polynote-backend/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/polynote-backend/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/polynote-backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/polynote-blockchain/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | coverage
4 | coverage.json
5 | typechain
6 | typechain-types
7 |
8 | #Hardhat files
9 | cache
10 | artifacts
11 |
12 | # IDE files
13 | .idea
14 |
15 |
--------------------------------------------------------------------------------
/polynote-blockchain/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Scroll
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/polynote-blockchain/README.md:
--------------------------------------------------------------------------------
1 | # Scroll Contract Deployment Demo
2 |
3 | This project demonstrates how to use hardhat or foundry to deploy a contract in Scroll's rollup network. This project contains a simple contract that will lock a certain amount of Ether in the deployed contract for a specified amount of time.
4 |
5 |
6 | ## Prerequisites
7 |
8 | - Network setup: https://guide.scroll.io/user-guide/setup
9 |
10 |
11 | ## Deploy with Hardhat
12 |
13 | 1. If you haven't already, install [nodejs](https://nodejs.org/en/download/) and [yarn](https://classic.yarnpkg.com/lang/en/docs/install).
14 | 2. Run `yarn install` to install dependencies.
15 | 3. Create a `.env` file following the example `.env.example` in the root directory. Change `PRIVATE_KEY` to your own account private key in the `.env`.
16 | 4. Run `yarn compile` to compile the contract.
17 | 5. Run `yarn deploy:scrollTestnet` to deploy the contract on the Scroll Alpha Testnet.
18 | 6. Run `yarn test` for hardhat tests.
19 |
20 |
21 | ## Deploy with Foundry
22 |
23 | 1. Install Foundry.
24 | ```shell
25 | curl -L https://foundry.paradigm.xyz | bash
26 | foundryup
27 | ```
28 | 2. Build the project.
29 | ```
30 | forge build
31 | ```
32 | 3. Deploy the contract.
33 | ```
34 | forge create --rpc-url https://alpha-rpc.scroll.io/l2 \
35 | --value \
36 | --constructor-args \
37 | --private-key \
38 | --legacy \
39 | contracts/Lock.sol:Lock
40 | ```
41 | - `` is the amount of `ETH` to be locked in the contract. Try setting this to some small amount, like `0.0000001ether`.
42 | - `` is the Unix timestamp after which the funds locked in the contract will become available for withdrawal. Try setting this to some Unix timestamp in the future, like `1696118400` (this Unix timestamp corresponds to October 1, 2023).
43 |
44 | For example:
45 | ```
46 | forge create --rpc-url https://alpha-rpc.scroll.io/l2 --value 0.00000000002ether --constructor-args 1696118400 --private-key 0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 --legacy contracts/Lock.sol:Lock
47 | ```
48 |
49 | ## Support
50 |
51 | Join our Discord: https://scroll.io/
52 |
--------------------------------------------------------------------------------
/polynote-blockchain/contracts/Polynote.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.9;
3 |
4 | contract Polynote {
5 | mapping(address => mapping(string => address[])) sharedAddresses;
6 |
7 | event Shared(address owner, string noteId, address partner);
8 | event Unshared(address owner, string noteId, address partner);
9 |
10 | function setPartners(
11 | string memory _noteId,
12 | address[] memory _partners
13 | ) public {
14 | sharedAddresses[msg.sender][_noteId] = _partners;
15 | for (uint256 i = 0; i < _partners.length; ++i) {
16 | emit Shared(msg.sender, _noteId, _partners[i]);
17 | }
18 | }
19 |
20 | function isShared(
21 | address _owner,
22 | string memory _noteId,
23 | address _partner
24 | ) public view returns (bool) {
25 | bool shared;
26 | for (uint i = 0; i < sharedAddresses[_owner][_noteId].length; ++i) {
27 | if (sharedAddresses[_owner][_noteId][i] == _partner) {
28 | shared = true;
29 | }
30 | }
31 | return shared;
32 | }
33 |
34 | function getSharedAddresses(
35 | address _owner,
36 | string memory _noteId
37 | ) public view returns (address[] memory) {
38 | return sharedAddresses[_owner][_noteId];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/polynote-blockchain/deployments.ts:
--------------------------------------------------------------------------------
1 | export const deployments = {
2 | scrollMainnet: "0xc6Ab1437507B4156b4DbFbe061b369f0973Be8F4",
3 | };
4 |
--------------------------------------------------------------------------------
/polynote-blockchain/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | ffi = false
3 | fuzz_runs = 256
4 | optimizer = true
5 | optimizer_runs = 1000000
6 | remappings = [
7 | "solmate/=lib/solmate/src/",
8 | "forge-std/=lib/forge-std/src/"
9 | ]
10 | verbosity = 1
11 |
12 | # Extreme Fuzzing CI Profile :P
13 | [profile.ci]
14 | fuzz_runs = 100_000
15 | verbosity = 4
16 |
--------------------------------------------------------------------------------
/polynote-blockchain/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 |
3 | import { HardhatUserConfig } from "hardhat/config";
4 | import "@nomicfoundation/hardhat-toolbox";
5 |
6 | dotenv.config();
7 |
8 | const config: HardhatUserConfig = {
9 | solidity: {
10 | version: "0.8.9",
11 | settings: {
12 | optimizer: {
13 | enabled: true,
14 | runs: 200,
15 | },
16 | },
17 | },
18 | networks: {
19 | scrollTestnet: {
20 | url: process.env.SCROLL_TESTNET_URL || "",
21 | accounts:
22 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
23 | },
24 | scrollMainnet: {
25 | url: process.env.SCROLL_MAINNET_URL || "",
26 | accounts:
27 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
28 | },
29 | },
30 | };
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/polynote-blockchain/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scroll-deploy-contract-demo",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "compile": "npx hardhat compile",
8 | "test": "npx hardhat test",
9 | "clean": "npx hardhat clean",
10 | "deploy:hh": "npx hardhat run --network hardhat scripts/deploy.ts",
11 | "deploy:scrollTestnet": "npx hardhat run --network scrollTestnet scripts/deploy.ts",
12 | "deploy:scrollMainnet": "npx hardhat run --network scrollMainnet scripts/deploy.ts"
13 | },
14 | "devDependencies": {
15 | "@ethersproject/abi": "^5.4.7",
16 | "@ethersproject/providers": "^5.4.7",
17 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0",
18 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
19 | "@nomicfoundation/hardhat-toolbox": "^1.0.1",
20 | "@nomiclabs/hardhat-ethers": "^2.0.0",
21 | "@nomiclabs/hardhat-etherscan": "^3.0.0",
22 | "@typechain/ethers-v5": "^10.1.0",
23 | "@typechain/hardhat": "^6.1.2",
24 | "@types/chai": "^4.2.0",
25 | "@types/mocha": "^9.1.0",
26 | "@types/node": ">=12.0.0",
27 | "chai": "^4.2.0",
28 | "dotenv": "^16.0.1",
29 | "ethers": "^5.4.7",
30 | "hardhat": "^2.10.2",
31 | "hardhat-gas-reporter": "^1.0.8",
32 | "solidity-coverage": "^0.7.21",
33 | "ts-node": ">=8.0.0",
34 | "typechain": "^8.1.0",
35 | "typescript": ">=4.5.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/polynote-blockchain/scripts/deploy.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "hardhat";
2 |
3 | async function main() {
4 | const Polynote = await ethers.getContractFactory("Polynote");
5 | const polynote = await Polynote.deploy();
6 |
7 | await polynote.deployed();
8 |
9 | console.log(`Polynote contract deployer to ${polynote.address}`);
10 | // console.log(`Block explorer URL: https://l2scan.scroll.io/address/${lock.address}`); Uncomment here to use the pre-alpha
11 | console.log(
12 | `Block explorer URL: https://blockscout.scroll.io/address/${polynote.address}`
13 | );
14 | }
15 |
16 | // We recommend this pattern to be able to use async/await everywhere
17 | // and properly handle errors.
18 | main().catch((error) => {
19 | console.error(error);
20 | process.exitCode = 1;
21 | });
22 |
--------------------------------------------------------------------------------
/polynote-blockchain/test/Polynote.ts:
--------------------------------------------------------------------------------
1 | import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
2 | import { expect } from "chai";
3 | import { ethers } from "hardhat";
4 |
5 | describe("Lock", function () {
6 | // We define a fixture to reuse the same setup in every test.
7 | // We use loadFixture to run this setup once, snapshot that state,
8 | // and reset Hardhat Network to that snapshot in every test.
9 | async function deployFixture() {
10 | // Contracts are deployed using the first signer/account by default
11 | const [deployer, user1, user2] = await ethers.getSigners();
12 |
13 | const Polynote = await ethers.getContractFactory("Polynote");
14 | const polynote = await Polynote.deploy();
15 |
16 | return { deployer, user1, user2, Polynote, polynote };
17 | }
18 |
19 | describe("Deployment", function () {
20 | it("Not shared in the beginning", async function () {
21 | const { polynote, user1, user2 } = await loadFixture(deployFixture);
22 |
23 | expect(
24 | await polynote.isShared(user1.getAddress(), "0", user2.getAddress())
25 | ).to.equal(false);
26 | });
27 |
28 | it("Share with partner", async function () {
29 | const { polynote, user1, user2 } = await loadFixture(deployFixture);
30 | await polynote.connect(user1).setPartners("0", [user2.getAddress()]);
31 | expect(
32 | await polynote.isShared(user1.getAddress(), "0", user2.getAddress())
33 | ).to.equal(true);
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/polynote-blockchain/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/polynote-frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "@next/next/no-img-element":"off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/polynote-frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
--------------------------------------------------------------------------------
/polynote-frontend/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies = false
2 |
--------------------------------------------------------------------------------
/polynote-frontend/README.md:
--------------------------------------------------------------------------------
1 | This is a [RainbowKit](https://rainbowkit.com) + [wagmi](https://wagmi.sh) + [Next.js](https://nextjs.org/) project bootstrapped with [`create-rainbowkit`](https://github.com/rainbow-me/rainbowkit/tree/main/packages/create-rainbowkit).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | ```
10 |
11 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
12 |
13 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
14 |
15 | ## Learn More
16 |
17 | To learn more about this stack, take a look at the following resources:
18 |
19 | - [RainbowKit Documentation](https://rainbowkit.com) - Learn how to customize your wallet connection flow.
20 | - [wagmi Documentation](https://wagmi.sh) - Learn how to interact with Ethereum.
21 | - [Next.js Documentation](https://nextjs.org/docs) - Learn how to build a Next.js application.
22 |
23 | You can check out [the RainbowKit GitHub repository](https://github.com/rainbow-me/rainbowkit) - your feedback and contributions are welcome!
24 |
25 | ## Deploy on Vercel
26 |
27 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
28 |
29 | Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
30 |
--------------------------------------------------------------------------------
/polynote-frontend/assets/connect-page-illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/connect-page-illustration.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/empty-state-illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/empty-state-illustration.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/features-illustration-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/features-illustration-light.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/features-illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/features-illustration.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/flying.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/flying.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/landing-illustrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/landing-illustrations.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/logo/logo-large-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/logo/logo-large-white.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/logo/logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/logo/logo-large.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/logo/logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/logo/logo-small.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/filecoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/filecoin.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/nest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/nest.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/polybase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/polybase.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/push.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/push.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/react.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/scroll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/scroll.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/partners/tiptap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/partners/tiptap.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/screens-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/screens-light.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/screens-sm-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/screens-sm-light.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/screens-sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/screens-sm.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/screens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/screens.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/team/alim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/team/alim.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/team/bugra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/team/bugra.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/team/eylul.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/team/eylul.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/team/farhad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/team/farhad.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/team/taylan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/team/taylan.png
--------------------------------------------------------------------------------
/polynote-frontend/assets/wallpaper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/assets/wallpaper.png
--------------------------------------------------------------------------------
/polynote-frontend/components/AccountModal/AccountModal.tsx:
--------------------------------------------------------------------------------
1 | import { usePolybaseUserQuery } from "restapi/queries";
2 | import { Button, Modal, Typography } from "ui";
3 | import LogoLargeWhite from "assets/logo/logo-large-white.png";
4 | import LogoLarge from "assets/logo/logo-large.png";
5 | import Image from "next/image";
6 | import { useAccount, useSignMessage } from "wagmi";
7 | import { useCreatePolybaseUserMutation } from "restapi/queries/useCreatePolybaseUserMutation";
8 | import { useTheme } from "recoil/theme/ThemeStoreHooks";
9 | import { usePolybaseUser, useSetToken } from "recoil/user/UserStoreHooks";
10 | import { useEffect, useRef } from "react";
11 | import { ACCESS_TOKEN_KEY } from "consts/storage";
12 | import { useAuthUserMutation } from "restapi/queries/useAuthUserMutation";
13 | import { useRouter } from "next/router";
14 | import { Paths } from "consts/paths";
15 | import { Messages } from "consts";
16 |
17 | export const AccountModal = () => {
18 | const { isLoading } = usePolybaseUserQuery();
19 | const theme = useTheme();
20 | const { address } = useAccount();
21 | const polybaseUser = usePolybaseUser();
22 | const router = useRouter();
23 | const checkedRef = useRef(false);
24 | const setToken = useSetToken();
25 |
26 | const createPolybaseUserMutation = useCreatePolybaseUserMutation();
27 | const authUserMutation = useAuthUserMutation();
28 |
29 | const {
30 | isLoading: isSigningRegistration,
31 | signMessage: signMessageForRegister,
32 | } = useSignMessage({
33 | message: Messages.CREATE_ACCOUNT,
34 | onSuccess: (res) => {
35 | createPolybaseUserMutation.mutate({
36 | address: address as string,
37 | signature: res,
38 | });
39 | },
40 | onError: () => {
41 | router.replace(Paths.CONNECT_WALLET);
42 | },
43 | });
44 |
45 | const { signMessage: signMessageForAuth } = useSignMessage({
46 | onSuccess: (res) => {
47 | authUserMutation.mutate({
48 | address: address as string,
49 | signature: res,
50 | });
51 | },
52 | onError: () => {
53 | router.replace(Paths.CONNECT_WALLET);
54 | },
55 | message: Messages.SIGN_IN,
56 | });
57 |
58 | useEffect(() => {
59 | if (checkedRef.current) {
60 | return;
61 | }
62 | if (polybaseUser?.address != null) {
63 | const access_token = localStorage.getItem(ACCESS_TOKEN_KEY);
64 | if (access_token == null) {
65 | signMessageForAuth();
66 | checkedRef.current = true;
67 | } else {
68 | setToken(access_token);
69 | }
70 | }
71 | // eslint-disable-next-line
72 | }, [polybaseUser?.address, setToken]);
73 |
74 | return (
75 | undefined,
82 | open: () => undefined,
83 | }}
84 | >
85 |
86 |
91 |
96 | Create your account on Polynote by signing the message below:
97 |
98 | Register
99 |
100 |
112 |
113 |
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Container/Container.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-left: auto;
3 | margin-right: auto;
4 |
5 | @media only screen and (max-width: 1440px) {
6 | max-width: 1380px;
7 | }
8 | @media only screen and (max-width: 1400px) {
9 | max-width: 1320px;
10 | }
11 | @media only screen and (max-width: 1340px) {
12 | max-width: 1300px;
13 | }
14 | @media only screen and (max-width: 1320px) {
15 | max-width: 1180px;
16 | }
17 | @media only screen and (max-width: 1200px) {
18 | max-width: 1120px;
19 | }
20 | @media only screen and (max-width: 1148px) {
21 | max-width: 1020px;
22 | }
23 | @media only screen and (max-width: 1024px) {
24 | max-width: 980px;
25 | }
26 | @media only screen and (max-width: 992px) {
27 | max-width: 840px;
28 | }
29 | @media only screen and (max-width: 860px) {
30 | max-width: 92vw;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Container/Container.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef, CSSProperties, RefObject } from "react";
2 | import { clsnm } from "utils/clsnm";
3 | import styles from "./Container.module.scss";
4 |
5 | interface ContainerProps extends ComponentPropsWithoutRef<"div"> {
6 | width?: number;
7 | className?: string;
8 | styles?: CSSProperties;
9 | forwardedRef?: RefObject;
10 | }
11 |
12 | const Container = ({
13 | children,
14 | width = 1280,
15 | style,
16 | className,
17 | forwardedRef,
18 | }: ContainerProps) => {
19 | return (
20 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export { Container };
34 |
--------------------------------------------------------------------------------
/polynote-frontend/components/CreateNoteModal/CreateNoteModal.tsx:
--------------------------------------------------------------------------------
1 | import { ModalController } from "hooks/useModal";
2 | import { Button, Input, Modal, Typography } from "ui";
3 | import data from "@emoji-mart/data";
4 | import Picker from "@emoji-mart/react";
5 | import { useTheme } from "recoil/theme/ThemeStoreHooks";
6 | import { useDropdown } from "hooks/useDropdown";
7 | import { useEffect, useMemo, useState } from "react";
8 | import { useCreateNoteMutation } from "restapi/queries/useCreateNoteMutation";
9 | import { useAccount } from "wagmi";
10 |
11 | type Props = {
12 | modalController: ModalController;
13 | };
14 |
15 | export const CreateNoteModal = ({ modalController }: Props) => {
16 | const { address } = useAccount();
17 | const theme = useTheme();
18 | const [title, setTitle] = useState("");
19 | const [emoji, setEmoji] = useState("➕");
20 |
21 | const { reference, popperStyles, floating, isOpen, toggle, close, closeRef } =
22 | useDropdown({
23 | strategy: "fixed",
24 | topDistance: 12,
25 | placement: "bottom-start",
26 | });
27 |
28 | useEffect(() => {
29 | if (!modalController.isOpen) {
30 | setTitle("");
31 | setEmoji("➕");
32 | }
33 | }, [modalController.isOpen]);
34 |
35 | const createNoteMutation = useCreateNoteMutation({
36 | onSuccess: () => modalController.close(),
37 | });
38 |
39 | const isSubmitDisabled = useMemo(() => {
40 | if (title.trim() === "" || emoji === "➕") {
41 | return true;
42 | }
43 | return false;
44 | }, [title, emoji]);
45 |
46 | return (
47 |
52 |
53 |
54 | Create note
55 |
56 |
57 |
58 |
59 | Emoji describing your note
60 |
61 |
62 |
69 |
70 | {isOpen && (
71 |
72 |
{
76 | setEmoji(res.native);
77 | close();
78 | }}
79 | />
80 |
81 | )}
82 |
83 |
84 |
setTitle(e.target.value)}
90 | />
91 |
107 |
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Editor/Editor.tsx:
--------------------------------------------------------------------------------
1 | import { EditorContent, Editor as EditorProp } from "@tiptap/react";
2 | import { Dispatch, SetStateAction, useEffect } from "react";
3 | import { Note } from "recoil/notes/types";
4 |
5 | import { FloatingMenuEditor } from "./FloatingMenuEditor";
6 | import { BubbleMenuEditor } from "components/Editor/BubbleMenuEditor";
7 | import { ModalController } from "hooks/useModal";
8 | import { EditorSelectionRange } from "types";
9 |
10 | type Props = {
11 | editor: EditorProp;
12 | selectedNoteCopy: Note;
13 | setSelectedNoteCopy: Dispatch>;
14 | modalController: ModalController;
15 | };
16 |
17 | export const Editor = ({
18 | selectedNoteCopy,
19 | setSelectedNoteCopy,
20 | editor,
21 | modalController,
22 | }: Props) => {
23 | useEffect(() => {
24 | if (editor == null) {
25 | return;
26 | }
27 |
28 | const changeFn = (e: any) => {
29 | setSelectedNoteCopy({ ...selectedNoteCopy, content: e.editor.getHTML() });
30 | };
31 |
32 | editor.on("update", changeFn);
33 |
34 | return () => {
35 | editor.off("update", changeFn);
36 | };
37 | }, [editor, selectedNoteCopy, setSelectedNoteCopy]);
38 |
39 | return (
40 | <>
41 | {editor && }
42 |
43 | {editor && }
44 |
45 |
50 | >
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { CONFIG } from "config";
2 | import { NextSeo } from "next-seo";
3 |
4 | type Props = {
5 | title?: string;
6 | description?: string;
7 | };
8 |
9 | export const Header = ({ title, description }: Props) => {
10 | let _title = CONFIG.APP;
11 | if (title != null) {
12 | _title += ` - ${title}`;
13 | }
14 |
15 | return (
16 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Main/Main.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar } from "components/Sidebar/Sidebar";
2 | import {
3 | useNotes,
4 | useSelectedNote,
5 | useSetSelectedNote,
6 | } from "recoil/notes/NotesStoreHooks";
7 | import { useNotesQuery } from "restapi/queries";
8 | import LogoLargeWhite from "assets/logo/logo-large-white.png";
9 | import LogoLarge from "assets/logo/logo-large.png";
10 | import { useTheme } from "recoil/theme/ThemeStoreHooks";
11 | import Image from "next/image";
12 | import { Button, Typography } from "ui";
13 | import { BsPlus } from "react-icons/bs";
14 | import { CreateNoteModal } from "components/CreateNoteModal/CreateNoteModal";
15 | import { ModalController, useModal } from "hooks/useModal";
16 | import { NoteEditor } from "components/NoteEditor/NoteEditor";
17 | import { NoteHeader } from "components/NoteHeader/NoteHeader";
18 | import { useEffect, useRef, useState } from "react";
19 | import { useRouter } from "next/router";
20 | import EmptyStateIllustration from "assets/empty-state-illustration.png";
21 |
22 | export const Main = () => {
23 | useNotesQuery();
24 | const selectedNote = useSelectedNote();
25 | const setSelectedNote = useSetSelectedNote();
26 | const createNoteModal = useModal();
27 | const [updating, setUpdating] = useState(false);
28 | const router = useRouter();
29 | const notes = useNotes();
30 | const openedRef = useRef(false);
31 |
32 | useEffect(() => {
33 | if (selectedNote?.id != null) {
34 | window.location.hash = `#id=${selectedNote.id}`;
35 | }
36 | }, [selectedNote?.id, router]);
37 |
38 | useEffect(() => {
39 | if (openedRef.current === true) {
40 | return;
41 | }
42 |
43 | const hash = window.location.hash;
44 |
45 | if (hash != null) {
46 | const split = hash.split("id=");
47 | const id = split[1];
48 | const find = notes.find((note) => note.id === id);
49 | if (find != null) {
50 | setSelectedNote(find);
51 | openedRef.current = true;
52 | }
53 | }
54 | }, [notes, setSelectedNote]);
55 |
56 | return (
57 | <>
58 |
59 |
60 |
61 |
62 | {selectedNote != null && (
63 |
64 |
65 |
69 |
70 | )}
71 |
72 | {notes.length === 0 && (
73 |
74 | )}
75 | {notes.length > 0 && selectedNote == null && (
76 |
77 | )}
78 |
79 |
80 | >
81 | );
82 | };
83 |
84 | const EmptyState = ({
85 | createNoteModal,
86 | select,
87 | }: {
88 | createNoteModal: ModalController;
89 | select: boolean;
90 | }) => {
91 | const theme = useTheme();
92 |
93 | return (
94 |
95 |
101 |
106 | {select ? (
107 |
112 | You can select note from sidebar to start editing
113 |
114 | ) : (
115 | }
118 | color={theme === "dark" ? "primary" : "secondary"}
119 | className="h-10 w-full mt-4"
120 | >
121 | Create note
122 |
123 | )}
124 |
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/polynote-frontend/components/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Logo from "assets/logo/logo-large.png";
2 | import LogoWhite from "assets/logo/logo-large-white.png";
3 | import { Container } from "components";
4 | import { Paths } from "consts/paths";
5 | import Image from "next/image";
6 | import { useRouter } from "next/router";
7 | import { AiOutlineWallet } from "react-icons/ai";
8 | import { useTheme, useToggleTheme } from "recoil/theme/ThemeStoreHooks";
9 | import { Button } from "ui";
10 | import { BsFillMoonFill, BsSunFill } from "react-icons/bs";
11 |
12 | export const Navbar = () => {
13 | const router = useRouter();
14 | const theme = useTheme();
15 | const toggleTheme = useToggleTheme();
16 |
17 | return (
18 |
19 |
20 |
21 |
22 | router.push("/")}
24 | alt="Logo"
25 | src={theme === "dark" ? LogoWhite : Logo}
26 | className="w-full shrink-0"
27 | />
28 |
29 |
30 |
38 |
:
}
41 | onClick={toggleTheme}
42 | color="primary"
43 | />
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/polynote-frontend/components/NoteHeader/NoteHeader.tsx:
--------------------------------------------------------------------------------
1 | import { ShareModal } from "components";
2 | import { useModal } from "hooks/useModal";
3 | import { useMemo } from "react";
4 | import { BsChevronLeft, BsChevronRight, BsTrash } from "react-icons/bs";
5 | import { useNotes, useSetSelectedNote } from "recoil/notes/NotesStoreHooks";
6 | import { Note } from "recoil/notes/types";
7 | import { useTheme } from "recoil/theme/ThemeStoreHooks";
8 | import { useDeleteNoteMutation } from "restapi/queries/useDeleteNoteMutation";
9 | import { Button, Spinner, Typography } from "ui";
10 |
11 | type Props = { selectedNote: Note; updating: boolean };
12 |
13 | export const NoteHeader = ({ selectedNote, updating }: Props) => {
14 | const theme = useTheme();
15 | const notes = useNotes();
16 | const setSelectedNote = useSetSelectedNote();
17 | const shareModal = useModal();
18 |
19 | const currentIndex = useMemo(() => {
20 | let index = -1;
21 | for (let i = 0; i < notes.length; i++) {
22 | if (selectedNote.id === notes[i].id) {
23 | index = i;
24 | break;
25 | }
26 | }
27 | return index;
28 | }, [notes, selectedNote]);
29 |
30 | const increaseIndex = () => {
31 | const nextIndex = currentIndex + 1;
32 | if (nextIndex < notes.length) {
33 | setSelectedNote(notes[nextIndex]);
34 | } else {
35 | setSelectedNote(notes[0]);
36 | }
37 | };
38 |
39 | const decreaseIndex = () => {
40 | const prevIndex = currentIndex - 1;
41 | if (prevIndex >= 0) {
42 | setSelectedNote(notes[prevIndex]);
43 | } else {
44 | setSelectedNote(notes[notes.length - 1]);
45 | }
46 | };
47 |
48 | const deleteNoteMutation = useDeleteNoteMutation();
49 |
50 | return (
51 | <>
52 | {shareModal.isOpen && (
53 |
54 | )}
55 |
56 | {notes.length > 1 && (
57 |
58 |
65 |
72 |
73 | )}
74 |
75 |
76 | {updating ? (
77 |
78 | ) : (
79 |
84 | last updated at{" "}
85 | {new Date(selectedNote.updated * 1000).toLocaleString()}
86 |
87 | )}
88 |
89 |
96 |
97 |
105 |
106 | >
107 | );
108 | };
109 |
--------------------------------------------------------------------------------
/polynote-frontend/components/SettingsModal/SettingsModal.tsx:
--------------------------------------------------------------------------------
1 | import { ModalController } from "hooks/useModal";
2 | import { useEffect, useState } from "react";
3 | import { AiOutlineUser } from "react-icons/ai";
4 | import { useTheme, useToggleTheme } from "recoil/theme/ThemeStoreHooks";
5 | import { usePolybaseUser } from "recoil/user/UserStoreHooks";
6 | import { useUpdatePolybaseUserNameMutation } from "restapi/queries";
7 | import { Button, Input, Modal, Typography } from "ui";
8 | import { BsFillMoonFill, BsSunFill } from "react-icons/bs";
9 |
10 | type Props = {
11 | modalController: ModalController;
12 | };
13 |
14 | export const SettingsModal = ({ modalController }: Props) => {
15 | const theme = useTheme();
16 | const toggleTheme = useToggleTheme();
17 | const polybaseUser = usePolybaseUser();
18 | const [editedName, setEditedName] = useState("");
19 |
20 | const updatePolybaseUserNameMutation = useUpdatePolybaseUserNameMutation({
21 | onSuccess: () => modalController.close(),
22 | });
23 |
24 | useEffect(() => {
25 | setEditedName(polybaseUser?.name ?? "");
26 | }, [polybaseUser?.name]);
27 |
28 | useEffect(() => {
29 | if (!modalController.isOpen) {
30 | setEditedName(polybaseUser?.name ?? "");
31 | }
32 | }, [modalController.isOpen, polybaseUser?.name]);
33 |
34 | return (
35 |
36 |
37 |
42 | Settings
43 |
44 |
45 |
setEditedName(e.target.value)}
48 | placeholder="Account name"
49 | label="Polynote account name"
50 | containerClassName="mt-4"
51 | icon={
}
52 | />
53 |
54 |
67 |
68 |
73 | Theme
74 |
75 |
76 |
:
}
78 | className="h-[40px] w-full mt-[8px]"
79 | onClick={toggleTheme}
80 | color={theme === "dark" ? "primary" : "secondary"}
81 | >
82 | {theme == "dark" ? "Dark" : "Light"}
83 |
84 |
85 |
86 | );
87 | };
88 |
--------------------------------------------------------------------------------
/polynote-frontend/components/index.ts:
--------------------------------------------------------------------------------
1 | export { Header } from "./Header/Header";
2 | export { Editor } from "./Editor/Editor";
3 | export { Sidebar } from "./Sidebar/Sidebar";
4 | export { SettingsModal } from "./SettingsModal/SettingsModal";
5 | export { AccountModal } from "./AccountModal/AccountModal";
6 | export { Main } from "./Main/Main";
7 | export { ShareModal } from "./ShareModal/ShareModal";
8 | export { NoteEditor } from "./NoteEditor/NoteEditor";
9 | export { NoteHeader } from "./NoteHeader/NoteHeader";
10 | export { Navbar } from "./Navbar/Navbar";
11 | export { Container } from "./Container/Container";
12 | export { AiModal } from "./AiModal/AiModal";
13 |
--------------------------------------------------------------------------------
/polynote-frontend/config.ts:
--------------------------------------------------------------------------------
1 | import { zksync_testnet } from "consts/chains";
2 |
3 | export const CONFIG = {
4 | APP: "Polynote",
5 | APP_DESCRIPTION: "Note taking application on Polybase",
6 | PUSH_CHANNEL_CAIP: "eip155:5:0xA63A810228a180767d3502EF8d21DbF4Da0D6b43",
7 | WC_PROJECT_ID: "13543b39fc73c43252acb941cc642dd3",
8 | CHAINS: [zksync_testnet],
9 | };
10 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/abi.ts:
--------------------------------------------------------------------------------
1 | export const POLYNOTE_ABI = [
2 | {
3 | anonymous: false,
4 | inputs: [
5 | {
6 | indexed: false,
7 | internalType: "address",
8 | name: "owner",
9 | type: "address",
10 | },
11 | {
12 | indexed: false,
13 | internalType: "string",
14 | name: "noteId",
15 | type: "string",
16 | },
17 | {
18 | indexed: false,
19 | internalType: "address",
20 | name: "partner",
21 | type: "address",
22 | },
23 | ],
24 | name: "Shared",
25 | type: "event",
26 | },
27 | {
28 | anonymous: false,
29 | inputs: [
30 | {
31 | indexed: false,
32 | internalType: "address",
33 | name: "owner",
34 | type: "address",
35 | },
36 | {
37 | indexed: false,
38 | internalType: "string",
39 | name: "noteId",
40 | type: "string",
41 | },
42 | {
43 | indexed: false,
44 | internalType: "address",
45 | name: "partner",
46 | type: "address",
47 | },
48 | ],
49 | name: "Unshared",
50 | type: "event",
51 | },
52 | {
53 | inputs: [
54 | {
55 | internalType: "address",
56 | name: "_owner",
57 | type: "address",
58 | },
59 | {
60 | internalType: "string",
61 | name: "_noteId",
62 | type: "string",
63 | },
64 | ],
65 | name: "getSharedAddresses",
66 | outputs: [
67 | {
68 | internalType: "address[]",
69 | name: "",
70 | type: "address[]",
71 | },
72 | ],
73 | stateMutability: "view",
74 | type: "function",
75 | },
76 | {
77 | inputs: [
78 | {
79 | internalType: "address",
80 | name: "_owner",
81 | type: "address",
82 | },
83 | {
84 | internalType: "string",
85 | name: "_noteId",
86 | type: "string",
87 | },
88 | {
89 | internalType: "address",
90 | name: "_partner",
91 | type: "address",
92 | },
93 | ],
94 | name: "isShared",
95 | outputs: [
96 | {
97 | internalType: "bool",
98 | name: "",
99 | type: "bool",
100 | },
101 | ],
102 | stateMutability: "view",
103 | type: "function",
104 | },
105 | {
106 | inputs: [
107 | {
108 | internalType: "string",
109 | name: "_noteId",
110 | type: "string",
111 | },
112 | {
113 | internalType: "address[]",
114 | name: "_partners",
115 | type: "address[]",
116 | },
117 | ],
118 | name: "setPartners",
119 | outputs: [],
120 | stateMutability: "nonpayable",
121 | type: "function",
122 | },
123 | ];
124 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/chains.ts:
--------------------------------------------------------------------------------
1 | import { Chain } from "viem";
2 |
3 | export const scroll_testnet = {
4 | id: 534353,
5 | network: "scroll",
6 | name: "Scroll",
7 | nativeCurrency: {
8 | name: "Scroll Ether",
9 | symbol: "ETH",
10 | decimals: 18,
11 | },
12 | rpcUrls: {
13 | default: {
14 | http: ["https://alpha-rpc.scroll.io/l2"],
15 | },
16 | public: {
17 | http: ["https://alpha-rpc.scroll.io/l2"],
18 | },
19 | },
20 | blockExplorers: {
21 | default: {
22 | name: "Blockscout",
23 | url: "https://blockscout.scroll.io",
24 | },
25 | },
26 | testnet: true,
27 | };
28 |
29 | export const zksync_testnet: Chain = {
30 | id: 300,
31 | network: "zksync",
32 | name: "zkSync Sepolia Testnet",
33 | nativeCurrency: {
34 | name: "Ether",
35 | symbol: "ETH",
36 | decimals: 18,
37 | },
38 | rpcUrls: {
39 | default: {
40 | http: ["https://sepolia.era.zksync.dev"],
41 | },
42 | public: {
43 | http: ["https://sepolia.era.zksync.dev"],
44 | },
45 | },
46 | blockExplorers: {
47 | default: {
48 | name: "zkSync Era Block Explorer",
49 | url: "https://sepolia.explorer.zksync.io/",
50 | },
51 | },
52 | testnet: true,
53 | };
54 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/contracts.ts:
--------------------------------------------------------------------------------
1 | export const POLYNOTE_CONTRACT = "0x7e615C0ad8C5BD894e42d3E3c7b0C533dFfC1cA0";
2 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/index.ts:
--------------------------------------------------------------------------------
1 | export { Messages } from "./messages";
2 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/messages.ts:
--------------------------------------------------------------------------------
1 | export enum Messages {
2 | SIGN_IN = "Polynote - Sign in",
3 | CREATE_ACCOUNT = "Polynote - Create account",
4 | SEE_SHARED_NOTE = "Polynote - See shared note",
5 | }
6 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/numbers.ts:
--------------------------------------------------------------------------------
1 | export const BYTES = 1024;
2 | export const MB = BYTES * 1024;
3 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/paths.ts:
--------------------------------------------------------------------------------
1 | export enum Paths {
2 | LANDING = "/",
3 | CONNECT_WALLET = "/connect",
4 | DASHBOARD = "/dashboard",
5 | SHARED = "/shared",
6 | }
7 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/storage.ts:
--------------------------------------------------------------------------------
1 | export const ACCESS_TOKEN_KEY = "POLYNOTE_ACCESS_TOKEN";
2 |
--------------------------------------------------------------------------------
/polynote-frontend/consts/upload.ts:
--------------------------------------------------------------------------------
1 | export const allowedFileTypes = ["png", "jpg", "jpeg", "svg", "webp"];
2 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useAvatar.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { getUserAvatar } from "utils/getAvatar";
3 | import { useAccount } from "wagmi";
4 |
5 | export const useAvatar = (size = 128) => {
6 | const [avatar, setAvatar] = useState("");
7 | const { address } = useAccount();
8 |
9 | useEffect(() => {
10 | if (address == null) return;
11 |
12 | setAvatar(getUserAvatar(address, size));
13 | }, [address, size]);
14 |
15 | return avatar;
16 | };
17 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useContractFunction.ts:
--------------------------------------------------------------------------------
1 | import { Contract, ContractInterface, ethers, Signer } from "ethers";
2 | import { useCallback, useMemo, useState } from "react";
3 | import { HexString } from "types";
4 | import { Abi } from "viem";
5 | import { usePublicClient, useWalletClient } from "wagmi";
6 |
7 | export const useContractFunction = ({
8 | address,
9 | abi,
10 | method,
11 | onFail,
12 | onSuccess,
13 | }: {
14 | address: string;
15 | abi: ContractInterface;
16 | method: string;
17 | onFail?: (err: unknown) => void;
18 | onSuccess?: (res: T) => void;
19 | }) => {
20 | const [isLoading, setIsLoading] = useState(false);
21 | const [isFailed, setIsFailed] = useState(false);
22 | const provider = usePublicClient();
23 | const signer = useWalletClient();
24 |
25 | const execute = useCallback(
26 | async >(
27 | type: "write" | "read" = "read",
28 | ...args: R
29 | ) => {
30 | const isRead = type === "read";
31 | try {
32 | setIsLoading(true);
33 | setIsFailed(false);
34 | let res;
35 | if (isRead) {
36 | res = await provider.readContract({
37 | abi: abi as Abi,
38 | address: address as HexString,
39 | functionName: method,
40 | args: args,
41 | });
42 | } else {
43 | const walletClient = signer.data;
44 | res = await walletClient?.writeContract({
45 | abi: abi as Abi,
46 | address: address as HexString,
47 | functionName: method,
48 | args: args,
49 | });
50 | }
51 |
52 | setIsLoading(false);
53 | onSuccess?.(res as T);
54 | if (isRead) {
55 | return res;
56 | }
57 | } catch (err) {
58 | setIsFailed(true);
59 | setIsLoading(false);
60 | onFail?.(err);
61 | console.error(err);
62 | }
63 | },
64 | [abi, address, method, onFail, onSuccess, provider, signer]
65 | );
66 |
67 | const write = useCallback(
68 | >(...args: T) => {
69 | execute("write", ...args);
70 | },
71 | [execute]
72 | );
73 |
74 | const read = useCallback(
75 | >(...args: T) => {
76 | return execute("read", ...args);
77 | },
78 | [execute]
79 | );
80 |
81 | const readPromise = useCallback(
82 | async = [], R = unknown>(...args: T) => {
83 | try {
84 | const res = await provider.readContract({
85 | abi: abi as Abi,
86 | address: address as HexString,
87 | functionName: method,
88 | args: args,
89 | });
90 | return res as R;
91 | } catch (err) {
92 | console.error(err);
93 | }
94 | },
95 | [abi, provider, address, method]
96 | );
97 |
98 | return useMemo(
99 | () => ({ isFailed, isLoading, read, readPromise, write }),
100 | [read, write, isLoading, isFailed, readPromise]
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useCopyText.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | export const useCopyText = () => {
4 | const [copied, setCopied] = useState(false);
5 |
6 | const onAddressCopied = useCallback((text?: string, result?: boolean) => {
7 | console.log("[COPY]", text, result);
8 | setCopied(true);
9 | const timeout = setTimeout(() => {
10 | setCopied(false);
11 | }, 2000);
12 | return () => {
13 | clearTimeout(timeout);
14 | };
15 | }, []);
16 |
17 | return [copied, onAddressCopied as any];
18 | };
19 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useDebounce(value: T, delay = 300) {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const handler = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 |
11 | return () => {
12 | clearTimeout(handler);
13 | };
14 | }, [value, delay]);
15 |
16 | return debouncedValue;
17 | }
18 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useDropdown.ts:
--------------------------------------------------------------------------------
1 | import { Placement } from "@floating-ui/react-dom";
2 | import { useModal } from "hooks/useModal";
3 | import { useOnClickOutside } from "hooks/useOnClickOutside";
4 | import { usePopper } from "hooks/usePopper";
5 | import { useCallback } from "react";
6 |
7 | type Props = {
8 | strategy?: "fixed" | "absolute";
9 | topDistance?: number;
10 | leftDistance?: number;
11 | placement?: Placement;
12 | };
13 |
14 | export const useDropdown = ({
15 | strategy,
16 | topDistance,
17 | leftDistance,
18 | placement,
19 | }: Props = {}) => {
20 | const { isOpen, open, close } = useModal();
21 |
22 | const closeRef = useOnClickOutside(() => {
23 | close();
24 | });
25 |
26 | const toggle = useCallback(() => {
27 | if (isOpen) {
28 | close();
29 | } else {
30 | open();
31 | }
32 | }, [isOpen, close, open]);
33 |
34 | const { floating, reference, popperStyles } = usePopper({
35 | leftDistance,
36 | placement,
37 | strategy,
38 | topDistance,
39 | });
40 |
41 | return {
42 | close,
43 | closeRef,
44 | floating,
45 | isOpen,
46 | open,
47 | popperStyles,
48 | reference,
49 | toggle,
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useInitializeTheme.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useSetTheme, useTheme } from "recoil/theme/ThemeStoreHooks";
3 |
4 | const identifier = "PolynoteTheme";
5 | const initialTheme = "light";
6 |
7 | export const useInitializeTheme = () => {
8 | const setTheme = useSetTheme();
9 | const theme = useTheme();
10 |
11 | useEffect(() => {
12 | const existingTheme = localStorage.getItem(identifier);
13 |
14 | if (existingTheme == null) {
15 | localStorage.setItem(identifier, initialTheme);
16 | } else {
17 | if (existingTheme === "dark") {
18 | setTheme("dark");
19 | } else if (existingTheme === "light") {
20 | setTheme("light");
21 | }
22 | }
23 | }, [setTheme]);
24 |
25 | useEffect(() => {
26 | if (theme == null) {
27 | return;
28 | }
29 |
30 | const htmlElement = document.documentElement;
31 | const bodyElement = document.body;
32 |
33 | htmlElement.classList.add(theme);
34 | htmlElement.classList.remove(theme === "dark" ? "light" : "dark");
35 |
36 | bodyElement.classList.add(theme);
37 | bodyElement.classList.remove(theme === "dark" ? "light" : "dark");
38 |
39 | localStorage.setItem(identifier, theme);
40 | }, [theme]);
41 | };
42 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useListenNetworkChange.ts:
--------------------------------------------------------------------------------
1 | import { CONFIG } from "config";
2 | import { scroll_testnet } from "consts/chains";
3 | import { useEffect } from "react";
4 | import { useSwitchNetwork } from "wagmi";
5 |
6 | export const useListenNetworkChange = (
7 | interval = 2000,
8 | pauseExecution = false
9 | ) => {
10 | const { switchNetwork } = useSwitchNetwork({
11 | chainId: CONFIG.CHAINS[0].id,
12 | });
13 |
14 | useEffect(() => {
15 | const timeout = setInterval(() => {
16 | if (pauseExecution) {
17 | return;
18 | }
19 |
20 | switchNetwork?.();
21 | }, interval);
22 |
23 | return () => {
24 | clearInterval(timeout);
25 | };
26 | }, [switchNetwork, interval, pauseExecution]);
27 | };
28 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useModal.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from "react";
2 |
3 | export type ModalController = {
4 | isOpen: boolean;
5 | open: () => void;
6 | close: () => void;
7 | };
8 |
9 | /**
10 | * @dev Hook used to control modal state
11 | */
12 | export const useModal = (): ModalController => {
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | const open = useCallback(() => setIsOpen(true), []);
16 | const close = useCallback(() => setIsOpen(false), []);
17 |
18 | const modal = useMemo(() => ({ close, isOpen, open }), [isOpen, open, close]);
19 |
20 | return modal;
21 | };
22 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useNotify.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { toast } from "react-toastify";
3 |
4 | const COLORS = {
5 | error: "#dc2626",
6 | info: "#6366f1",
7 | success: "#16a34a",
8 | warn: "#facc15",
9 | };
10 |
11 | export const useNotify = () => {
12 | const notifier = useMemo(() => {
13 | const _notify = (text?: string, color?: string) => {
14 | if (text == null) {
15 | console.warn("Notify should take a text argument");
16 | return;
17 | }
18 | toast(text, { progressStyle: { background: color } });
19 | };
20 | return {
21 | error: (text?: string) => _notify(text, COLORS.error),
22 | info: (text?: string) => _notify(text, COLORS.info),
23 | success: (text?: string) => _notify(text, COLORS.success),
24 | warn: (text?: string) => _notify(text, COLORS.warn),
25 | };
26 | }, []);
27 |
28 | return notifier;
29 | };
30 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useOnAccountsChange.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { useAccount } from "wagmi";
3 |
4 | export const useOnAccountsChange = (callback: () => void) => {
5 | const { address } = useAccount();
6 |
7 | const firstAddressRef = useRef(null);
8 | useEffect(() => {
9 | if (address == null || firstAddressRef.current != null) return;
10 | firstAddressRef.current = address;
11 | }, [address]);
12 |
13 | useEffect(() => {
14 | if (firstAddressRef.current == null || address == null) return;
15 |
16 | if (firstAddressRef.current != address) {
17 | callback?.();
18 | firstAddressRef.current = address;
19 | }
20 | }, [address, callback]);
21 | };
22 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/useOnClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export function useOnClickOutside(
4 | handler: (val: any) => void,
5 | elements?: any
6 | ) {
7 | const ref = useRef(null);
8 |
9 | useEffect(() => {
10 | const listener = (event: any) => {
11 | // Do nothing if clicking ref's element or descendent elements
12 | if (
13 | (!ref.current || ref.current.contains(event.target)) &&
14 | (!elements ||
15 | elements.filter((el: any) => el?.contains?.(event.target)).length > 0)
16 | ) {
17 | return;
18 | }
19 |
20 | handler(event);
21 | };
22 |
23 | document.addEventListener("mousedown", listener);
24 | document.addEventListener("touchstart", listener);
25 |
26 | return () => {
27 | document.removeEventListener("mousedown", listener);
28 | document.removeEventListener("touchstart", listener);
29 | };
30 | }, [handler, elements]);
31 |
32 | return ref;
33 | }
34 |
--------------------------------------------------------------------------------
/polynote-frontend/hooks/usePopper.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Placement,
3 | autoUpdate,
4 | useFloating,
5 | shift,
6 | } from "@floating-ui/react-dom";
7 |
8 | type UsePopperProps = {
9 | strategy?: "fixed" | "absolute";
10 | topDistance?: number;
11 | leftDistance?: number;
12 | placement?: Placement;
13 | };
14 |
15 | /**
16 | * @dev Used for popper dropdowns
17 | */
18 | export const usePopper = ({
19 | strategy = "fixed",
20 | placement = "bottom-start",
21 | topDistance = 0,
22 | leftDistance = 0,
23 | }: UsePopperProps = {}) => {
24 | const {
25 | x,
26 | y,
27 | reference,
28 | floating,
29 | strategy: floatingStrategy,
30 | } = useFloating({
31 | middleware: [shift()],
32 | placement: placement,
33 | strategy: strategy,
34 | whileElementsMounted: autoUpdate,
35 | });
36 |
37 | const _top = placement.includes("bottom")
38 | ? (y || 0) + topDistance
39 | : (y || 0) - topDistance;
40 |
41 | const _left = placement.includes("right")
42 | ? (x || 0) + leftDistance
43 | : (x || 0) - leftDistance;
44 |
45 | const popperStyles = {
46 | left: _left,
47 | position: floatingStrategy,
48 | top: _top,
49 | zIndex: 10,
50 | };
51 |
52 | return { floating, popperStyles, reference };
53 | };
54 |
--------------------------------------------------------------------------------
/polynote-frontend/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/polynote-frontend/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/polynote-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polynote-frontend-v1",
3 | "private": true,
4 | "version": "0.1.0",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@dicebear/avatars": "4.10.8",
13 | "@dicebear/avatars-human-sprites": "4.10.8",
14 | "@emoji-mart/data": "1.1.2",
15 | "@emoji-mart/react": "1.1.1",
16 | "@floating-ui/react-dom": "1.3.0",
17 | "@polybase/util": "0.3.36",
18 | "@progress/kendo-react-upload": "^5.12.0",
19 | "@pushprotocol/restapi": "^0.8.3",
20 | "@rainbow-me/rainbowkit": "^1.1.3",
21 | "@tiptap/extension-color": "^2.0.0-beta.220",
22 | "@tiptap/extension-dropcursor": "^2.0.0-beta.220",
23 | "@tiptap/extension-image": "^2.0.0-beta.220",
24 | "@tiptap/extension-link": "^2.0.0-beta.220",
25 | "@tiptap/extension-list-item": "^2.0.0-beta.220",
26 | "@tiptap/extension-placeholder": "2.0.0-beta.220",
27 | "@tiptap/extension-text-style": "^2.0.0-beta.220",
28 | "@tiptap/pm": "2.0.0-beta.220",
29 | "@tiptap/react": "2.0.0-beta.220",
30 | "@tiptap/starter-kit": "^2.0.0-beta.220",
31 | "axios": "1.3.4",
32 | "emoji-mart": "5.5.2",
33 | "ethers": "5.7.1",
34 | "minimist": "^1.2.8",
35 | "next": "13.0.6",
36 | "next-seo": "5.15.0",
37 | "nextjs-progressbar": "0.0.16",
38 | "process": "^0.11.10",
39 | "react": "18.2.0",
40 | "react-dom": "18.2.0",
41 | "react-icons": "4.8.0",
42 | "react-query": "3.39.3",
43 | "react-toastify": "9.1.1",
44 | "recoil": "0.7.7",
45 | "viem": "^1.16.6",
46 | "wagmi": "^1.4.5",
47 | "web3.storage": "^4.5.4",
48 | "zksync-web3": "^0.14.3"
49 | },
50 | "devDependencies": {
51 | "@types/node": "17.0.35",
52 | "@types/react": "18.0.9",
53 | "@types/react-copy-to-clipboard": "^5.0.4",
54 | "autoprefixer": "^10.4.13",
55 | "eslint": "8.15.0",
56 | "eslint-config-next": "12.1.6",
57 | "postcss": "8.4.21",
58 | "react-copy-to-clipboard": "^5.1.0",
59 | "sass": "1.55.0",
60 | "tailwindcss": "3.2.7",
61 | "typescript": "^5.0.4"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/polynote-frontend/pages/connect.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from "next";
2 | import Wallpaper from "assets/wallpaper.png";
3 | import ConnectPageIllustration from "assets/connect-page-illustration.png";
4 | import LogoLargeWhite from "assets/logo/logo-large-white.png";
5 | import Image from "next/image";
6 | import { CSSProperties, useEffect } from "react";
7 | import { Button, CustomConnectButton } from "ui";
8 | import { useAccount } from "wagmi";
9 | import { Typography } from "ui/Typography/Typography";
10 | import { formatAddress } from "utils/formatAddress";
11 | import { useRouter } from "next/router";
12 | import { Paths } from "consts/paths";
13 | import { Header } from "components";
14 | import { useSetPolybaseUser } from "recoil/user/UserStoreHooks";
15 |
16 | const borderStyles: CSSProperties = {
17 | borderTopLeftRadius: "180px",
18 | borderTopRightRadius: "180px",
19 | };
20 |
21 | const ConnectPage: NextPage = () => {
22 | const { isConnected, address } = useAccount();
23 | const setPolybaseUser = useSetPolybaseUser();
24 | const router = useRouter();
25 |
26 | useEffect(() => {
27 | setPolybaseUser(null);
28 | }, [setPolybaseUser]);
29 |
30 | return (
31 | <>
32 |
33 |
37 |
38 |
39 |
40 |
44 |
48 |
49 |
50 | {isConnected && address != null ? (
51 |
52 |
57 | You are connected as{" "}
58 |
59 | {formatAddress(address)}
60 |
61 |
62 |
63 |
70 |
71 |
76 |
77 | ) : (
78 |
82 | )}
83 |
84 |
85 |
86 | >
87 | );
88 | };
89 |
90 | export default ConnectPage;
91 |
--------------------------------------------------------------------------------
/polynote-frontend/pages/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { AccountModal, Header, Main } from "components";
2 | import { Paths } from "consts/paths";
3 | import { NextPage } from "next";
4 | import { useRouter } from "next/router";
5 | import { useEffect } from "react";
6 | import { useToken } from "recoil/user/UserStoreHooks";
7 | import { Spinner, Typography } from "ui";
8 | import { useAccount } from "wagmi";
9 |
10 | const Dashboard: NextPage = () => {
11 | const { isConnected, isReconnecting, isConnecting } = useAccount();
12 | const router = useRouter();
13 | const token = useToken();
14 |
15 | useEffect(() => {
16 | if (isReconnecting || isConnecting) return;
17 | if (!isConnected) {
18 | router.replace(Paths.CONNECT_WALLET);
19 | }
20 | }, [isConnected, router, isReconnecting, isConnecting]);
21 |
22 | return (
23 | <>
24 |
25 | {isConnected && }
26 | {token != null && isConnected ? (
27 |
28 | ) : (
29 |
30 |
31 |
35 | Signing in...
36 |
37 |
38 | )}
39 | >
40 | );
41 | };
42 |
43 | export default Dashboard;
44 |
--------------------------------------------------------------------------------
/polynote-frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/polynote-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itublockchain/polynote-scaling2023/826beb9a9677f036fd5d5f9b2f0d5e8928f5bbb1/polynote-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/polynote-frontend/recoil/notes/NotesStore.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { Note } from "recoil/notes/types";
3 |
4 | export const NotesAtom = atom({
5 | default: [],
6 | key: "Notes.Atom",
7 | });
8 |
9 | export const SelectedNoteAtom = atom({
10 | default: null,
11 | key: "Note.Atom",
12 | });
13 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/notes/NotesStoreHooks.ts:
--------------------------------------------------------------------------------
1 | import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from "recoil";
2 | import { NotesAtom, SelectedNoteAtom } from "recoil/notes/NotesStore";
3 | import { Note } from "recoil/notes/types";
4 |
5 | export const useNotes = (): Note[] => {
6 | return useRecoilValue(NotesAtom);
7 | };
8 |
9 | export const useSetNotes = (): SetterOrUpdater => {
10 | return useSetRecoilState(NotesAtom);
11 | };
12 |
13 | export const useSelectedNote = (): Note | null => {
14 | return useRecoilValue(SelectedNoteAtom);
15 | };
16 |
17 | export const useSetSelectedNote = (): SetterOrUpdater => {
18 | return useSetRecoilState(SelectedNoteAtom);
19 | };
20 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/notes/types.ts:
--------------------------------------------------------------------------------
1 | export type Note = {
2 | id: string;
3 | address: string;
4 | emoji: string;
5 | title: string;
6 | content: string;
7 | created: number;
8 | updated: number;
9 | };
10 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/theme/ThemeStore.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { ThemeOption } from "recoil/theme/types";
3 |
4 | export const ThemeAtom = atom({
5 | default: "light",
6 | key: "Theme.Atom",
7 | });
8 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/theme/ThemeStoreHooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from "recoil";
3 | import { ThemeAtom } from "recoil/theme/ThemeStore";
4 | import { ThemeOption } from "recoil/theme/types";
5 |
6 | export const useTheme = (): ThemeOption => {
7 | return useRecoilValue(ThemeAtom);
8 | };
9 |
10 | export const useSetTheme = (): SetterOrUpdater => {
11 | return useSetRecoilState(ThemeAtom);
12 | };
13 |
14 | export const useToggleTheme = () => {
15 | const theme = useTheme();
16 | const setTheme = useSetTheme();
17 |
18 | return useCallback(() => {
19 | if (theme === "dark") {
20 | setTheme("light");
21 | } else {
22 | setTheme("dark");
23 | }
24 | }, [theme, setTheme]);
25 | };
26 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/theme/types.ts:
--------------------------------------------------------------------------------
1 | export type ThemeOption = "dark" | "light";
2 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/user/UserStore.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { PolybaseUser } from "restapi/types";
3 |
4 | export const UserAtom = atom({
5 | default: null,
6 | key: "PolbaseUser.Atom",
7 | });
8 |
9 | export const TokenAtom = atom({
10 | default: null,
11 | key: "Token.Atom",
12 | });
13 |
--------------------------------------------------------------------------------
/polynote-frontend/recoil/user/UserStoreHooks.ts:
--------------------------------------------------------------------------------
1 | import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from "recoil";
2 | import { TokenAtom, UserAtom } from "recoil/user/UserStore";
3 | import { PolybaseUser } from "restapi/types";
4 |
5 | export const usePolybaseUser = (): PolybaseUser | null => {
6 | return useRecoilValue(UserAtom);
7 | };
8 |
9 | export const useSetPolybaseUser = (): SetterOrUpdater => {
10 | return useSetRecoilState(UserAtom);
11 | };
12 |
13 | export const useToken = (): string | null => {
14 | return useRecoilValue(TokenAtom);
15 | };
16 |
17 | export const useSetToken = (): SetterOrUpdater => {
18 | return useSetRecoilState(TokenAtom);
19 | };
20 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/index.ts:
--------------------------------------------------------------------------------
1 | import Axios from "axios";
2 | import {
3 | AiTextDto,
4 | AuthUserDto,
5 | CreateNoteDto,
6 | CreatePolybaseUserDto,
7 | PushNotificationDto,
8 | UpdateNoteDto,
9 | UpdatePolybaseUserNameDto,
10 | } from "restapi/types";
11 | import { getToken } from "utils/getToken";
12 | import { AITextDto } from "./types";
13 |
14 | const apiURL =
15 | process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000/api/v1";
16 |
17 | const axios = Axios.create({
18 | baseURL: apiURL,
19 | });
20 |
21 | axios.interceptors.request.use(
22 | function (config) {
23 | config.headers.Authorization = getToken();
24 | return config;
25 | },
26 | function (error) {
27 | // Do something with request error
28 | return Promise.reject(error);
29 | }
30 | );
31 |
32 | export const apiGetPolybaseUser = (address: string) => {
33 | return axios.get(`/user/${address}`);
34 | };
35 |
36 | export const apiGetNotes = (address: string) => {
37 | return axios.get(`/notes/${address}`);
38 | };
39 |
40 | export const apiUpdatePolybaseUserName = (
41 | address: string,
42 | data: UpdatePolybaseUserNameDto
43 | ) => {
44 | return axios.put(`/user/${address}`, data);
45 | };
46 |
47 | export const apiCreatePolybaseUser = (data: CreatePolybaseUserDto) => {
48 | return axios.post(`/user`, data);
49 | };
50 |
51 | export const apiAuthUser = (data: AuthUserDto) => {
52 | return axios.post(`/user/auth`, data);
53 | };
54 |
55 | export const apiCreateNote = (data: CreateNoteDto) => {
56 | return axios.post(`/notes`, data);
57 | };
58 |
59 | export const apiUpdateNote = (id: string, data: UpdateNoteDto) => {
60 | return axios.post(`/notes/${id}`, data);
61 | };
62 |
63 | export const apiDeleteNote = (id: string) => {
64 | return axios.delete(`/notes/${id}`);
65 | };
66 |
67 | export const apiGetSharedNote = (
68 | id: string,
69 | data: { address: string; signature: string }
70 | ) => {
71 | return axios.post(`/notes/shared/${id}`, data);
72 | };
73 |
74 | export const apiOptInNotifications = (data: PushNotificationDto) => {
75 | return axios.post(`/user/notifications/opt-in`, data);
76 | };
77 |
78 | export const apiOptOutNotificationsO = (data: PushNotificationDto) => {
79 | return axios.post(`/user/notifications/opt-out`, data);
80 | };
81 |
82 | export const apiUploadFile = (data: FormData) => {
83 | return axios.post(`/upload`, data);
84 | };
85 |
86 | export const apiAI = (data: AiTextDto) => {
87 | return axios.post(`/ai/`, data);
88 | };
89 |
90 | export const POLYBASE_USER_QUERY = ["polybase_user"];
91 | export const NOTES_QUERY = ["notes"];
92 | export const NOTIFICATIONS = ["notifications"];
93 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/index.ts:
--------------------------------------------------------------------------------
1 | export { usePolybaseUserQuery } from "./usePolybaseUserQuery";
2 | export { useUpdatePolybaseUserNameMutation } from "./useUpdatePolybaseUserNameMutation";
3 | export { useAuthUserMutation } from "./useAuthUserMutation";
4 | export { useCreatePolybaseUserMutation } from "./useCreatePolybaseUserMutation";
5 | export { useNotesQuery } from "./useNotesQuery";
6 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useAiMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { useMutation } from "react-query";
6 | import { useSetToken } from "recoil/user/UserStoreHooks";
7 | import { apiAI } from "restapi";
8 | import { AiTextDto } from "restapi/types";
9 | import { useAiModalContext } from "utils/AiModalContext";
10 |
11 | export const useAiMutation = () => {
12 | const router = useRouter();
13 | const setToken = useSetToken();
14 | const aiModalContext = useAiModalContext();
15 |
16 | const mutation = useMutation({
17 | mutationFn: (data: AiTextDto) => apiAI(data),
18 | onSuccess: (res: AxiosResponse<{ text: string }>) => {
19 | const suggestion = res.data.text;
20 | let split = suggestion.split("\n");
21 | split = split.filter((item) => item.trim() != "");
22 | aiModalContext.setSuggestion(split.join(" "));
23 | },
24 | onError: (err: AxiosError) => {
25 | if (err.response?.status === HttpStatusCode.Unauthorized) {
26 | router.replace(Paths.CONNECT_WALLET);
27 | localStorage.removeItem(ACCESS_TOKEN_KEY);
28 | setToken(null);
29 | }
30 | },
31 | });
32 |
33 | return mutation;
34 | };
35 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useAuthUserMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { useMutation } from "react-query";
6 | import { useSetToken } from "recoil/user/UserStoreHooks";
7 | import { apiAuthUser } from "restapi";
8 | import { AuthUserDto } from "restapi/types";
9 |
10 | export const useAuthUserMutation = () => {
11 | const setToken = useSetToken();
12 | const router = useRouter();
13 |
14 | const mutation = useMutation({
15 | mutationFn: (data: AuthUserDto) => apiAuthUser(data),
16 | onSuccess: (res) => {
17 | setToken(res.data.token);
18 | localStorage.setItem(ACCESS_TOKEN_KEY, res.data.token);
19 | },
20 | onError: (err: AxiosError) => {
21 | if (err.response?.status === HttpStatusCode.Unauthorized) {
22 | router.replace(Paths.CONNECT_WALLET);
23 | localStorage.removeItem(ACCESS_TOKEN_KEY);
24 | setToken(null);
25 | }
26 | },
27 | retry: false,
28 | });
29 |
30 | return mutation;
31 | };
32 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useCreateNoteMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { queryClient } from "pages/_app";
6 | import { useMutation, useQuery } from "react-query";
7 | import { useSetSelectedNote } from "recoil/notes/NotesStoreHooks";
8 | import { useSetToken } from "recoil/user/UserStoreHooks";
9 | import { apiCreateNote, NOTES_QUERY } from "restapi";
10 | import { CreateNoteDto } from "restapi/types";
11 |
12 | export const useCreateNoteMutation = (
13 | {
14 | onSuccess,
15 | }: {
16 | onSuccess?: () => void;
17 | } = { onSuccess: () => undefined }
18 | ) => {
19 | const setSelectedNote = useSetSelectedNote();
20 | const router = useRouter();
21 | const setToken = useSetToken();
22 |
23 | const mutation = useMutation({
24 | mutationFn: (data: CreateNoteDto) => apiCreateNote(data),
25 | onSuccess: (res) => {
26 | queryClient.refetchQueries(NOTES_QUERY);
27 |
28 | setSelectedNote(res.data);
29 |
30 | onSuccess?.();
31 | },
32 | onError: (err: AxiosError) => {
33 | if (err.response?.status === HttpStatusCode.Unauthorized) {
34 | router.replace(Paths.CONNECT_WALLET);
35 | localStorage.removeItem(ACCESS_TOKEN_KEY);
36 | setToken(null);
37 | }
38 | },
39 | });
40 |
41 | return mutation;
42 | };
43 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useCreatePolybaseUserMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { queryClient } from "pages/_app";
6 | import { useMutation } from "react-query";
7 | import { useSetToken } from "recoil/user/UserStoreHooks";
8 | import { apiCreatePolybaseUser, POLYBASE_USER_QUERY } from "restapi";
9 | import { CreatePolybaseUserDto } from "restapi/types";
10 |
11 | export const useCreatePolybaseUserMutation = () => {
12 | const router = useRouter();
13 | const setToken = useSetToken();
14 |
15 | const mutation = useMutation({
16 | mutationFn: (data: CreatePolybaseUserDto) => apiCreatePolybaseUser(data),
17 | onSuccess: () => {
18 | queryClient.refetchQueries(POLYBASE_USER_QUERY);
19 | },
20 | onError: (err: AxiosError) => {
21 | if (err.response?.status === HttpStatusCode.Unauthorized) {
22 | router.replace(Paths.CONNECT_WALLET);
23 | localStorage.removeItem(ACCESS_TOKEN_KEY);
24 | setToken(null);
25 | }
26 | },
27 | });
28 |
29 | return mutation;
30 | };
31 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useDeleteNoteMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { useMutation } from "react-query";
6 | import {
7 | useNotes,
8 | useSetNotes,
9 | useSetSelectedNote,
10 | } from "recoil/notes/NotesStoreHooks";
11 | import { useSetToken } from "recoil/user/UserStoreHooks";
12 | import { apiDeleteNote } from "restapi";
13 |
14 | export const useDeleteNoteMutation = () => {
15 | const setSelectedNote = useSetSelectedNote();
16 | const setNotes = useSetNotes();
17 | const notes = useNotes();
18 | const router = useRouter();
19 | const setToken = useSetToken();
20 |
21 | const mutation = useMutation({
22 | mutationFn: (id: string) => apiDeleteNote(id),
23 | onSuccess: (_, id) => {
24 | const newNotes = notes.filter((item) => item.id !== id);
25 | setNotes(newNotes);
26 | if (newNotes.length > 0) {
27 | setSelectedNote(newNotes[newNotes.length - 1]);
28 | } else {
29 | setSelectedNote(null);
30 | }
31 | },
32 | onError: (err: AxiosError) => {
33 | if (err.response?.status === HttpStatusCode.Unauthorized) {
34 | router.replace(Paths.CONNECT_WALLET);
35 | localStorage.removeItem(ACCESS_TOKEN_KEY);
36 | setToken(null);
37 | }
38 | },
39 | });
40 |
41 | return mutation;
42 | };
43 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useNotesQuery.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { useQuery } from "react-query";
6 | import { useSetNotes } from "recoil/notes/NotesStoreHooks";
7 | import { Note } from "recoil/notes/types";
8 | import { useSetToken } from "recoil/user/UserStoreHooks";
9 | import { apiGetNotes, NOTES_QUERY } from "restapi";
10 | import { useAccount } from "wagmi";
11 |
12 | export const useNotesQuery = () => {
13 | const { address } = useAccount();
14 | const setNotes = useSetNotes();
15 | const router = useRouter();
16 | const setToken = useSetToken();
17 |
18 | const { data, ...rest } = useQuery({
19 | queryKey: NOTES_QUERY,
20 | queryFn: () =>
21 | apiGetNotes(address as string).then((res: AxiosResponse) => {
22 | const notes = res.data;
23 |
24 | const _notes: Note[] = [];
25 |
26 | for (const note of notes) {
27 | if (
28 | note.id != null &&
29 | note.address != null &&
30 | note.title != null &&
31 | note.emoji != null &&
32 | note.content != null &&
33 | note.created != null &&
34 | note.updated != null
35 | ) {
36 | _notes.push({
37 | id: note.id,
38 | address: note.address,
39 | content: note.content,
40 | emoji: note.emoji,
41 | title: note.title,
42 | created: note.created,
43 | updated: note.updated,
44 | });
45 | }
46 | }
47 |
48 | const sortedNotes = _notes.sort((a, b) => {
49 | return a.created - b.created;
50 | });
51 |
52 | setNotes(sortedNotes);
53 | return sortedNotes;
54 | }),
55 | cacheTime: 0,
56 | refetchOnWindowFocus: false,
57 | onError: (err: AxiosError) => {
58 | if (err.response?.status === HttpStatusCode.Unauthorized) {
59 | router.replace(Paths.CONNECT_WALLET);
60 | localStorage.removeItem(ACCESS_TOKEN_KEY);
61 | setToken(null);
62 | }
63 | },
64 | });
65 |
66 | return { notes: data, ...rest };
67 | };
68 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useOptInMutation.ts:
--------------------------------------------------------------------------------
1 | import { useNotify } from "hooks/useNotify";
2 | import { useAccount, useChainId, useSwitchNetwork } from "wagmi";
3 | import { useState } from "react";
4 |
5 | export const useOptInMutation = (
6 | {
7 | onSuccess,
8 | }: {
9 | onSuccess?: () => void;
10 | } = { onSuccess: () => undefined }
11 | ) => {
12 | const notify = useNotify();
13 | const { address } = useAccount();
14 | const [isLoading, setIsLoading] = useState(false);
15 | const { switchNetworkAsync } = useSwitchNetwork();
16 | const chainId = useChainId();
17 |
18 | const optIn = async () => {};
19 |
20 | return { optIn, isLoading };
21 | };
22 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useOptOutMutation.ts:
--------------------------------------------------------------------------------
1 | import { useNotify } from "hooks/useNotify";
2 | import { useState } from "react";
3 | import {
4 | goerli,
5 | useAccount,
6 | useChainId,
7 | useSigner,
8 | useSwitchNetwork,
9 | } from "wagmi";
10 | import * as PushAPI from "@pushprotocol/restapi";
11 | import { ENV } from "@pushprotocol/restapi/src/lib/constants";
12 | import { CONFIG } from "config";
13 |
14 | export const useOptOutMutation = (
15 | {
16 | onSuccess,
17 | }: {
18 | onSuccess?: () => void;
19 | } = { onSuccess: () => undefined }
20 | ) => {
21 | const notify = useNotify();
22 | const { data: signer } = useSigner();
23 | const { address } = useAccount();
24 | const [isLoading, setIsLoading] = useState(false);
25 | const { switchNetworkAsync } = useSwitchNetwork();
26 | const chainId = useChainId();
27 |
28 | const optOut = async () => {
29 | if (chainId !== goerli.id) {
30 | await switchNetworkAsync?.(goerli.id);
31 | }
32 | setIsLoading(true);
33 |
34 | await PushAPI.channels.unsubscribe({
35 | signer: signer as any,
36 | channelAddress: CONFIG.PUSH_CHANNEL_CAIP,
37 | userAddress: `eip155:5:${address}`,
38 | onSuccess: async () => {
39 | onSuccess?.();
40 | notify.success("Successfully opted out");
41 | },
42 | onError: () => {
43 | notify.error("Failed to opt out");
44 | },
45 | env: ENV.STAGING,
46 | });
47 |
48 | setIsLoading(false);
49 | };
50 |
51 | return { optOut, isLoading };
52 | };
53 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/usePolybaseUserQuery.ts:
--------------------------------------------------------------------------------
1 | import { Paths } from "consts/paths";
2 | import { useRouter } from "next/router";
3 | import { useEffect } from "react";
4 | import { useQuery } from "react-query";
5 | import { useSetPolybaseUser } from "recoil/user/UserStoreHooks";
6 | import { apiGetPolybaseUser, POLYBASE_USER_QUERY } from "restapi";
7 | import { PolybaseUser } from "restapi/types";
8 | import { useAccount } from "wagmi";
9 |
10 | export const usePolybaseUserQuery = () => {
11 | const { address } = useAccount();
12 | const setPolybaseUser = useSetPolybaseUser();
13 | const router = useRouter();
14 |
15 | const { data, ...rest } = useQuery({
16 | queryKey: POLYBASE_USER_QUERY,
17 | queryFn: () =>
18 | apiGetPolybaseUser(address as string).then((res) => {
19 | const user = res.data.user;
20 |
21 | if (user == null) {
22 | return null;
23 | }
24 |
25 | return user;
26 | }),
27 | onError: () => {
28 | router.replace(Paths.CONNECT_WALLET);
29 | },
30 | enabled: address != null,
31 | cacheTime: 0,
32 | refetchOnWindowFocus: false,
33 | });
34 |
35 | useEffect(() => {
36 | setPolybaseUser(data);
37 | }, [data, setPolybaseUser]);
38 |
39 | return { user: data as PolybaseUser | null, ...rest };
40 | };
41 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/usePushNotifications.ts:
--------------------------------------------------------------------------------
1 | import { NOTIFICATIONS } from "restapi";
2 | import * as PushAPI from "@pushprotocol/restapi";
3 | import { ENV } from "@pushprotocol/restapi/src/lib/constants";
4 | import { useQuery } from "react-query";
5 | import { PushNotificationsResponseData } from "restapi/types";
6 |
7 | export const usePushNotifications = ({
8 | page = 1,
9 | address,
10 | }: {
11 | page?: number;
12 | address?: string;
13 | }) => {
14 | const { data: notifications, ...rest } = useQuery({
15 | queryKey: NOTIFICATIONS,
16 | queryFn: () =>
17 | PushAPI.user
18 | .getFeeds({
19 | user: `eip155:5:${address}`,
20 | env: ENV.STAGING,
21 | page,
22 | limit: 100,
23 | })
24 | .then((res: PushNotificationsResponseData[]) => {
25 | return res;
26 | }),
27 | cacheTime: 0,
28 | refetchOnWindowFocus: false,
29 | enabled: address != null,
30 | });
31 |
32 | return { notifications: notifications ?? [], ...rest };
33 | };
34 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useUpdateNoteMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from "react-query";
2 | import { Note } from "recoil/notes/types";
3 | import { UpdateNoteDto } from "restapi/types";
4 | import { apiUpdateNote } from "restapi";
5 | import { AxiosError, AxiosResponse, HttpStatusCode } from "axios";
6 | import { useNotes, useSetNotes } from "recoil/notes/NotesStoreHooks";
7 | import { ACCESS_TOKEN_KEY } from "consts/storage";
8 | import { Paths } from "consts/paths";
9 | import { useRouter } from "next/router";
10 | import { useSetToken } from "recoil/user/UserStoreHooks";
11 |
12 | export const useUpdateNoteMutation = (
13 | selectedNote: Note,
14 | setUpdating: (to: boolean) => void
15 | ) => {
16 | const setNotes = useSetNotes();
17 | const notes = useNotes();
18 | const router = useRouter();
19 | const setToken = useSetToken();
20 |
21 | const mutation = useMutation({
22 | mutationFn: (data: UpdateNoteDto) => apiUpdateNote(selectedNote.id, data),
23 | onSuccess: (res: AxiosResponse) => {
24 | const newNote = res.data;
25 |
26 | const newNotes = notes.map((item) => {
27 | if (item.id === newNote.id) {
28 | return newNote;
29 | } else {
30 | return item;
31 | }
32 | });
33 |
34 | setNotes(newNotes);
35 | },
36 | onSettled: () => {
37 | setUpdating(false);
38 | },
39 | onMutate: () => {
40 | setUpdating(true);
41 | },
42 | onError: (err: AxiosError) => {
43 | if (err.response?.status === HttpStatusCode.Unauthorized) {
44 | router.replace(Paths.CONNECT_WALLET);
45 | localStorage.removeItem(ACCESS_TOKEN_KEY);
46 | setToken(null);
47 | }
48 | },
49 | });
50 |
51 | return mutation;
52 | };
53 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useUpdatePolybaseUserNameMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, HttpStatusCode } from "axios";
2 | import { Paths } from "consts/paths";
3 | import { ACCESS_TOKEN_KEY } from "consts/storage";
4 | import { useRouter } from "next/router";
5 | import { queryClient } from "pages/_app";
6 | import { useMutation } from "react-query";
7 | import { useSetToken } from "recoil/user/UserStoreHooks";
8 | import { apiUpdatePolybaseUserName, POLYBASE_USER_QUERY } from "restapi";
9 | import { UpdatePolybaseUserNameDto } from "restapi/types";
10 | import { useAccount } from "wagmi";
11 |
12 | export const useUpdatePolybaseUserNameMutation = (
13 | {
14 | onSuccess,
15 | }: {
16 | onSuccess?: () => void;
17 | } = { onSuccess: () => undefined }
18 | ) => {
19 | const { address } = useAccount();
20 | const router = useRouter();
21 | const setToken = useSetToken();
22 |
23 | const mutation = useMutation({
24 | mutationFn: (data: UpdatePolybaseUserNameDto) =>
25 | apiUpdatePolybaseUserName(address as string, data),
26 | onSuccess: () => {
27 | queryClient.refetchQueries(POLYBASE_USER_QUERY);
28 | onSuccess?.();
29 | },
30 | onError: (err: AxiosError) => {
31 | if (err.response?.status === HttpStatusCode.Unauthorized) {
32 | router.replace(Paths.CONNECT_WALLET);
33 | localStorage.removeItem(ACCESS_TOKEN_KEY);
34 | setToken(null);
35 | }
36 | },
37 | });
38 |
39 | return mutation;
40 | };
41 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/queries/useUploadMutation.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosResponse } from "axios";
2 | import { useMutation } from "react-query";
3 | import { apiUploadFile } from "restapi";
4 |
5 | export const useUploadMutation = (
6 | {
7 | onSuccess,
8 | onError,
9 | }: {
10 | onSuccess?: (res: AxiosResponse) => void;
11 | onError?: (err: AxiosError) => void;
12 | } = { onSuccess: () => undefined, onError: () => undefined }
13 | ) => {
14 | const mutation = useMutation({
15 | mutationFn: (data: FormData) => apiUploadFile(data),
16 | onSuccess: (res) => {
17 | onSuccess?.(res);
18 | },
19 | onError: (err: AxiosError) => {
20 | onError?.(err);
21 | },
22 | });
23 |
24 | return mutation;
25 | };
26 |
--------------------------------------------------------------------------------
/polynote-frontend/restapi/types.ts:
--------------------------------------------------------------------------------
1 | import { AiModalMode } from "utils/AiModalContext";
2 |
3 | export type PolybaseUser = {
4 | address: string;
5 | name: string;
6 | id: string;
7 | };
8 |
9 | export type CreatePolybaseUserDto = {
10 | address: string;
11 | signature: string;
12 | };
13 |
14 | export type AuthUserDto = {
15 | address: string;
16 | signature: string;
17 | };
18 |
19 | export type UpdatePolybaseUserNameDto = {
20 | name: string;
21 | };
22 |
23 | export type CreateNoteDto = {
24 | address: string;
25 | emoji: string;
26 | title: string;
27 | content: string;
28 | };
29 |
30 | export type PushNotificationDto = {
31 | address: string;
32 | };
33 |
34 | export type AITextDto = {
35 | text: string;
36 | };
37 |
38 | export type UpdateNoteDto = {
39 | emoji: string;
40 | title: string;
41 | content: string;
42 | };
43 |
44 | export type PushNotificationsResponseData = {
45 | cta: string;
46 | title: string;
47 | message: string;
48 | icon: string;
49 | url: string;
50 | sid: string;
51 | app: string;
52 | image: string;
53 | blockchain: string;
54 | notification: {
55 | body: string;
56 | title: string;
57 | };
58 | secret: string;
59 | };
60 |
61 | export type AiTextDto = {
62 | text: string;
63 | mode: AiModalMode;
64 | };
65 |
--------------------------------------------------------------------------------
/polynote-frontend/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body {
7 | padding: 0;
8 | margin: 0;
9 | }
10 |
11 | a {
12 | color: inherit;
13 | text-decoration: none;
14 | }
15 |
16 | * {
17 | box-sizing: border-box;
18 | font-family: "Poppins", sans-serif;
19 | }
20 |
21 | body.light {
22 | background-color: #fafafa;
23 | }
24 |
25 | body.dark {
26 | background-color: #130b1b;
27 | }
28 |
29 | /* For Webkit-based browsers (Chrome, Safari and Opera) */
30 | .scrollbar-hide::-webkit-scrollbar {
31 | display: none;
32 | }
33 |
34 | /* For IE, Edge and Firefox */
35 | .scrollbar-hide {
36 | -ms-overflow-style: none; /* IE and Edge */
37 | scrollbar-width: none; /* Firefox */
38 | }
39 |
40 | .ProseMirror:focus {
41 | outline: none !important;
42 | }
43 |
44 | #ContentWrapper:focus {
45 | outline: none !important;
46 | }
47 |
48 | #ContentWrapper p.is-editor-empty:first-child::before {
49 | color: #adb5bd;
50 | content: attr(data-placeholder);
51 | float: left;
52 | height: 0;
53 | pointer-events: none;
54 | }
55 |
56 | .ProseMirror {
57 | > * + * {
58 | margin-top: 0.75em;
59 | }
60 |
61 | ul,
62 | ol {
63 | padding: 0 1rem;
64 | }
65 | }
66 |
67 | .ProseMirror {
68 | h1 {
69 | font-size: 40px;
70 | font-weight: 600;
71 | }
72 |
73 | h2 {
74 | font-size: 32px;
75 | font-weight: 600;
76 | }
77 |
78 | h3 {
79 | font-size: 24px;
80 | font-weight: 600;
81 | }
82 |
83 | h4 {
84 | font-size: 20px;
85 | font-weight: 600;
86 | }
87 |
88 | ul {
89 | list-style: disc;
90 | }
91 |
92 | ol {
93 | list-style: decimal;
94 | }
95 |
96 | blockquote {
97 | margin-left: 8px;
98 | font-style: italic;
99 | }
100 |
101 | a {
102 | color: rgb(89, 89, 255);
103 | }
104 |
105 | pre:has(code) {
106 | padding: 0.5rem;
107 | }
108 |
109 | img {
110 | margin-bottom: 12px !important;
111 | text-align: center;
112 | margin-left: auto;
113 | margin-right: auto;
114 | max-width: 100%;
115 | max-height: 400px;
116 |
117 | :focus {
118 | border: 1px solid blue;
119 | }
120 | }
121 | }
122 |
123 | .ProseMirror-selectednode {
124 | box-shadow: 0px 0px 0px 4px rgb(115, 115, 241) !important;
125 | }
126 |
127 | .ProseMirror-gapcursor:after {
128 | border-top: 4px solid rgb(115, 115, 241) !important;
129 | }
130 |
131 | body.light .polynote-image {
132 | color: black;
133 | }
134 |
135 | body.dark .polynote-image {
136 | color: white;
137 | }
138 |
139 | body.light .ProseMirror {
140 | pre:has(code) {
141 | background-color: rgb(229, 229, 229);
142 | }
143 | }
144 |
145 | body.dark .ProseMirror {
146 | pre:has(code) {
147 | background-color: #210c33;
148 | }
149 | }
150 |
151 | .bubble-menu {
152 | display: flex;
153 | padding: 0.5rem;
154 | border-radius: 0.5rem;
155 |
156 | button {
157 | border: none;
158 | background: none;
159 | color: #fff;
160 | font-size: 16px;
161 | font-weight: 400;
162 | padding: 0 0.2rem;
163 | opacity: 0.6;
164 |
165 | &:hover,
166 | &.is-active {
167 | opacity: 1;
168 | transform: scale(1.05);
169 | }
170 | }
171 | }
172 |
173 | .floating-menu {
174 | display: flex;
175 | padding: 0.2rem;
176 | border-radius: 0.5rem;
177 | transform: translateX(20px);
178 | width: max-content;
179 |
180 | button {
181 | border: none;
182 | background: none;
183 | color: #fff;
184 | font-size: 16px;
185 | font-weight: 400;
186 | padding: 0 0.2rem;
187 | opacity: 0.6;
188 | white-space: nowrap;
189 |
190 | &:hover,
191 | &.is-active {
192 | opacity: 1;
193 | transform: scale(1.05);
194 | }
195 | }
196 |
197 | &.updating {
198 | pointer-events: none;
199 | }
200 | }
201 |
202 | .rubik {
203 | font-family: "Rubik", sans-serif !important;
204 | }
205 |
206 | .animate-custom-bounce {
207 | transform: translateY(-10px);
208 | animation: bounce 5s infinite;
209 | }
210 |
211 | .polynote-outside {
212 | & .polynote-inside {
213 | display: none;
214 | }
215 |
216 | &:hover {
217 | & .polynote-inside {
218 | display: flex;
219 | }
220 | }
221 | }
222 |
223 | @keyframes bounce {
224 | 0% {
225 | transform: translateY(-10px);
226 | }
227 | 25% {
228 | transform: translateY(-30px);
229 | }
230 | 50% {
231 | transform: translateY(-10px);
232 | }
233 | 75% {
234 | transform: translateY(-30px);
235 | }
236 | 100% {
237 | transform: translateY(-10px);
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/polynote-frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./components/**/*.{js,ts,jsx,tsx}",
5 | "./pages/**/*.{js,ts,jsx,tsx}",
6 | "./containers/**/*.{js,ts,jsx,tsx}",
7 | "./ui/**/*.{js,ts,jsx,tsx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | MAIN_DARK: "#130b1b",
13 | DARK_PURPLE: "#373055",
14 | LIGHT_PURPLE: "#bd97e9",
15 | PURPLE: "#6f629e",
16 | PINK: "#f8c8ed",
17 | CONNECT_WINDOW: "#1D1029",
18 |
19 | buttonDangerBg: "#CA4F4F",
20 | buttonDangerBgHover: "#802323",
21 | buttonDangerColor: "#FFEEEE",
22 | buttonDarngetColorHover: "#F3F0FF",
23 |
24 | buttonSuccessBg: "#9DDD97",
25 | buttonSuccessBgHover: "#558351",
26 | buttonSuccessColor: "#225A0E",
27 | buttonSuccessColorHover: "#FFFFFF",
28 |
29 | buttonSecondaryBg: "#f8c8ed",
30 | buttonSecondaryBgHover: "#E9A2D8",
31 | buttonSecondaryColor: "#4B0A3C",
32 | buttonSecondaryColorHover: "#4B0A3C",
33 |
34 | buttonPrimaryBg: "#361151",
35 | buttonPrimaryBgHover: "#6B628E",
36 | buttonPrimaryColor: "#F3F0FF",
37 | buttonPrimaryColorHover: "#F3F0FF",
38 |
39 | sidebarLight: "#FFFEFF",
40 | sidebarDark: "#0E0616",
41 |
42 | emptyNoteBg: "#FFF0FC",
43 | sidebarNoteLight: "#FFE7F9",
44 | sidebarNoteDark: "#1E1427",
45 | },
46 | borderWidth: {
47 | 1: "1px",
48 | },
49 | height: {
50 | "screen-overflow": "calc(100vh + 120px)",
51 | "screen-half": "calc(100vh / 2)",
52 | },
53 | minHeight: {
54 | "screen-overflow": "calc(100vh + 120px)",
55 | },
56 | scale: {
57 | 102: "1.02",
58 | },
59 | },
60 | },
61 | darkMode: "class",
62 | plugins: [],
63 | };
64 |
--------------------------------------------------------------------------------
/polynote-frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true
18 | },
19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/polynote-frontend/types/index.ts:
--------------------------------------------------------------------------------
1 | export type EditorSelectionRange = {
2 | from: number;
3 | to: number;
4 | };
5 |
6 | export type HexString = `0x${string}`;
7 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef, ReactNode, RefObject, useMemo } from "react";
2 | import { Spinner } from "ui/Spinner/Spinner";
3 | import { clsnm } from "utils/clsnm";
4 |
5 | type ButtonColor = "primary" | "secondary" | "success" | "danger";
6 | type ButtonSize = "small" | "medium" | "large" | "xlarge";
7 |
8 | interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
9 | loading?: boolean;
10 | disabled?: boolean;
11 | leftIcon?: ReactNode;
12 | rightIcon?: ReactNode;
13 | forwardedRef?: RefObject;
14 | color: ButtonColor;
15 | className?: string;
16 | size?: ButtonSize;
17 | absolute?: boolean;
18 | }
19 |
20 | export function Button({
21 | loading,
22 | disabled,
23 | leftIcon,
24 | rightIcon,
25 | forwardedRef,
26 | className,
27 | children,
28 | color,
29 | size = "medium",
30 | absolute,
31 | ...props
32 | }: ButtonProps) {
33 | const colorAndSizeClassNames = useButtonClassName(size, color);
34 |
35 | return (
36 |
76 | );
77 | }
78 |
79 | // helpers
80 |
81 | export const useButtonClassName = (
82 | size: ButtonSize,
83 | color: ButtonColor
84 | ): string => {
85 | const colorAndSizeClassNames = useMemo((): string => {
86 | let arr: string[] = [];
87 |
88 | /**
89 | * Apply color styles
90 | */
91 | if (color === "primary") {
92 | arr = [
93 | "bg-buttonPrimaryBg",
94 | "hover:bg-buttonPrimaryBgHover",
95 | "text-buttonPrimaryColor",
96 | "hover:text-buttonPrimaryColorHover",
97 | ];
98 | } else if (color === "secondary") {
99 | arr = [
100 | "bg-buttonSecondaryBg",
101 | "hover:bg-buttonSecondaryBgHover",
102 | "text-buttonSecondaryColor",
103 | "hover:text-buttonSecondaryColorHover",
104 | ];
105 | } else if (color === "success") {
106 | arr = [
107 | "bg-buttonSuccessBg",
108 | "hover:bg-buttonSuccessBgHover",
109 | "text-buttonSuccessColor",
110 | "hover:text-buttonSuccessColorHover",
111 | ];
112 | } else if (color === "danger") {
113 | arr = [
114 | "bg-buttonDangerBg",
115 | "hover:bg-buttonDangerBgHover",
116 | "text-buttonDangerColor",
117 | "hover:text-buttonDangerColorHover",
118 | ];
119 | }
120 |
121 | return arr.join(" ");
122 | }, [color]);
123 |
124 | return colorAndSizeClassNames;
125 | };
126 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Button/CustomConnectButton.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectButton } from "@rainbow-me/rainbowkit";
2 | import { AiFillWarning, AiOutlineUser, AiOutlineWallet } from "react-icons/ai";
3 | import { Button } from "ui/Button/Button";
4 |
5 | export const CustomConnectButton = ({
6 | containerClassName,
7 | className,
8 | changeAccountText,
9 | disableAccountIcon,
10 | }: {
11 | containerClassName?: string;
12 | className?: string;
13 | changeAccountText?: string;
14 | disableAccountIcon?: boolean;
15 | }) => {
16 | return (
17 |
18 | {({
19 | account,
20 | chain,
21 | openAccountModal,
22 | openChainModal,
23 | openConnectModal,
24 | authenticationStatus,
25 | mounted,
26 | }) => {
27 | // Note: If your app doesn't use authentication, you
28 | // can remove all 'authenticationStatus' checks
29 | const ready = mounted && authenticationStatus !== "loading";
30 | const connected =
31 | ready &&
32 | account &&
33 | chain &&
34 | (!authenticationStatus || authenticationStatus === "authenticated");
35 |
36 | return (
37 |
48 | {(() => {
49 | if (!connected) {
50 | return (
51 |
}
54 | color="primary"
55 | onClick={openConnectModal}
56 | type="button"
57 | >
58 | Connect Wallet
59 |
60 | );
61 | }
62 |
63 | if (chain.unsupported) {
64 | return (
65 |
}
68 | color="secondary"
69 | onClick={openChainModal}
70 | type="button"
71 | >
72 | Wrong Network
73 |
74 | );
75 | }
76 |
77 | return (
78 |
79 | }
81 | className={className}
82 | color="primary"
83 | onClick={openAccountModal}
84 | type="button"
85 | >
86 | {changeAccountText ?? account.displayName}
87 |
88 |
89 | );
90 | })()}
91 |
92 | );
93 | }}
94 |
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
2 | import { Typography } from "ui/Typography/Typography";
3 | import { clsnm } from "utils/clsnm";
4 |
5 | interface InputProps extends ComponentPropsWithoutRef<"input"> {
6 | containerClassName?: string;
7 | containerRef?: RefObject;
8 | forwardedRef?: RefObject & any;
9 | extendHeight?: boolean;
10 | label?: ReactNode;
11 | error?: ReactNode;
12 | icon?: ReactNode;
13 | }
14 |
15 | export function Input({
16 | className,
17 | containerClassName,
18 | forwardedRef,
19 | containerRef,
20 | extendHeight = false,
21 | label,
22 | error,
23 | icon,
24 | ...props
25 | }: InputProps) {
26 | return (
27 |
28 | {label != null && (
29 | <>
30 | {typeof label === "string" ? (
31 |
36 | {label}
37 |
38 | ) : (
39 | label
40 | )}
41 | >
42 | )}
43 |
50 | {icon != null && (
51 |
52 | {icon}
53 |
54 | )}
55 |
56 |
66 | {error != null && (
67 | <>
68 | {typeof error === "string" ? (
69 |
74 | {error}
75 |
76 | ) : (
77 | error
78 | )}
79 | >
80 | )}
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Modal/Modal.module.scss:
--------------------------------------------------------------------------------
1 | .layout {
2 | z-index: 10000000;
3 | top: 0;
4 | left: 0;
5 | width: 100vw;
6 | height: 100vh;
7 | position: fixed;
8 | background: rgba(0, 0, 0, 0.4);
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | transition: 0.2s linear;
13 | animation: main 0.15s linear;
14 | }
15 | .body {
16 | position: relative;
17 | border-radius: 12px;
18 | padding: 1.5rem;
19 | animation: mainWithSlide 0.2s linear;
20 |
21 | @media (max-width: 768px) {
22 | padding: 1rem;
23 | max-width: 95vw !important;
24 | }
25 | }
26 | .close {
27 | position: absolute;
28 | top: 12px;
29 | right: 12px;
30 | cursor: pointer;
31 | width: 30px;
32 | height: 30px;
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | border-radius: 8px;
37 |
38 | &:hover {
39 | transform: scale(1.1);
40 | }
41 | }
42 |
43 | @keyframes main {
44 | from {
45 | opacity: 0;
46 | }
47 | to {
48 | opacity: 1;
49 | }
50 | }
51 |
52 | @keyframes mainWithSlide {
53 | from {
54 | opacity: 0;
55 | transform: translateY(200px);
56 | }
57 | to {
58 | opacity: 1;
59 | transform: translateY(0px);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { ModalController } from "hooks/useModal";
2 | import { useOnClickOutside } from "hooks/useOnClickOutside";
3 | import { ComponentPropsWithoutRef, ReactNode, useEffect } from "react";
4 | import { IoMdClose } from "react-icons/io";
5 | import { clsnm } from "utils/clsnm";
6 | import styles from "./Modal.module.scss";
7 |
8 | type ModalProps = {
9 | children: ReactNode;
10 | modalController: ModalController;
11 | closeOnClickOutside?: boolean;
12 | className?: string;
13 | bodyProps?: ComponentPropsWithoutRef<"div">;
14 | width?: string;
15 | showCloseIcon?: boolean;
16 | };
17 |
18 | const Modal = ({
19 | children,
20 | modalController,
21 | closeOnClickOutside = true,
22 | showCloseIcon = true,
23 | className,
24 | bodyProps = {},
25 | width = "320px",
26 | }: ModalProps) => {
27 | const { isOpen, close } = modalController;
28 |
29 | useEffect(() => {
30 | if (isOpen) {
31 | document.body.style.overflowY = "hidden";
32 | } else {
33 | document.body.style.overflowY = "auto";
34 | }
35 |
36 | return () => {
37 | document.body.style.overflowY = "auto";
38 | };
39 | }, [isOpen]);
40 |
41 | const outsideRef = useOnClickOutside(() => {
42 | if (closeOnClickOutside) {
43 | close();
44 | }
45 | });
46 |
47 | return isOpen ? (
48 |
52 |
61 | {showCloseIcon && (
62 |
69 |
70 |
71 | )}
72 | {children}
73 |
74 |
75 | ) : null;
76 | };
77 |
78 | export { Modal };
79 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Spinner/Spinner.module.scss:
--------------------------------------------------------------------------------
1 | .loader {
2 | border: 2px solid #f3f3f3;
3 | border-top: 2px solid;
4 | border-radius: 50%;
5 | animation: load 1s linear;
6 | animation-iteration-count: infinite;
7 | }
8 |
9 | @keyframes load {
10 | from {
11 | transform: rotate(0deg);
12 | }
13 | to {
14 | transform: rotate(360deg);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef, CSSProperties } from "react";
2 | import { clsnm } from "utils/clsnm";
3 | import styles from "./Spinner.module.scss";
4 |
5 | interface SpinnerProps extends ComponentPropsWithoutRef<"div"> {
6 | className?: string;
7 | size?: number;
8 | style?: CSSProperties;
9 | }
10 |
11 | const Spinner = ({
12 | className,
13 | size = 20,
14 | style = {},
15 | ...props
16 | }: SpinnerProps) => {
17 | return (
18 |
28 | );
29 | };
30 |
31 | export { Spinner };
32 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Typography/Typography.module.scss:
--------------------------------------------------------------------------------
1 | // Decoration
2 | .underline {
3 | text-decoration: underline;
4 | }
5 |
6 | // Weight
7 | .semibold {
8 | font-weight: 600;
9 | }
10 |
11 | .medium {
12 | font-weight: 500;
13 | }
14 |
15 | .regular {
16 | font-weight: 400;
17 | }
18 |
19 | // Variant
20 | .title1 {
21 | font-size: 2rem;
22 | line-height: 1.25;
23 | }
24 |
25 | .title2 {
26 | font-size: 1.75rem;
27 | line-height: 1.286;
28 | }
29 |
30 | .title3 {
31 | font-size: 1.5rem;
32 | line-height: 1.3334;
33 | }
34 |
35 | .title4 {
36 | font-size: 1.25rem;
37 | line-height: 1.4;
38 | }
39 |
40 | .title5 {
41 | font-size: 1.125rem;
42 | line-height: 1.3334;
43 | }
44 |
45 | .headline {
46 | font-size: 1rem;
47 | line-height: 1.5;
48 | }
49 |
50 | .body1 {
51 | font-size: 1rem;
52 | line-height: 1.5;
53 | }
54 |
55 | .body2 {
56 | font-size: 0.875rem;
57 | line-height: 1.429;
58 | }
59 |
60 | .caption {
61 | font-size: 0.75rem;
62 | line-height: 1.3334;
63 | }
64 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/Typography/Typography.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { clsnm } from "utils/clsnm";
3 |
4 | import styles from "./Typography.module.scss";
5 |
6 | type TypographyVariants =
7 | | "title1"
8 | | "title2"
9 | | "title3"
10 | | "title4"
11 | | "title5"
12 | | "headline"
13 | | "body1"
14 | | "body2"
15 | | "caption";
16 |
17 | type TypographyWeights = "semibold" | "medium" | "regular";
18 |
19 | type TypographyOwnProps = {
20 | as?: React.ElementType;
21 | variant: TypographyVariants;
22 | weight?: TypographyWeights;
23 | decor?: "underline";
24 | color?: string;
25 | style?: React.CSSProperties;
26 | };
27 |
28 | export type TypographyProps = TypographyOwnProps &
29 | Omit, keyof TypographyOwnProps>;
30 |
31 | const Typography = React.forwardRef((props: TypographyProps, ref) => {
32 | const {
33 | variant,
34 | weight,
35 | decor,
36 | as: Component = "span",
37 | className,
38 | children,
39 | style,
40 | color,
41 | ...rest
42 | } = props;
43 |
44 | return (
45 |
56 | {children}
57 |
58 | );
59 | });
60 |
61 | Typography.displayName = "Typography";
62 |
63 | export { Typography };
64 |
--------------------------------------------------------------------------------
/polynote-frontend/ui/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from "./Button/Button";
2 | export { CustomConnectButton } from "./Button/CustomConnectButton";
3 | export { Spinner } from "./Spinner/Spinner";
4 | export { Typography } from "./Typography/Typography";
5 | export { Input } from "./Input/Input";
6 | export { Modal } from "./Modal/Modal";
7 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/AiModalContext.tsx:
--------------------------------------------------------------------------------
1 | import { Editor } from "@tiptap/react";
2 | import React, {
3 | Dispatch,
4 | ReactNode,
5 | SetStateAction,
6 | useContext,
7 | useState,
8 | } from "react";
9 | import { EditorSelectionRange } from "types";
10 |
11 | export type AiModalMode = "make-longer" | "summarize" | "fix-grammar";
12 |
13 | export const AiModalContext = React.createContext<{
14 | selectionRange: EditorSelectionRange;
15 | setSelectionRange: Dispatch>;
16 | selection: string | null;
17 | setSelection: Dispatch>;
18 | mode: AiModalMode | null;
19 | setMode: Dispatch>;
20 | suggestion: string | null;
21 | setSuggestion: Dispatch>;
22 | editor: Editor | null;
23 | setEditor: Dispatch>;
24 | }>({
25 | selectionRange: { from: 0, to: 0 },
26 | setSelectionRange: () => undefined,
27 | selection: null,
28 | setSelection: () => undefined,
29 | mode: null,
30 | setMode: () => undefined,
31 | suggestion: null,
32 | setSuggestion: () => undefined,
33 | editor: null,
34 | setEditor: () => undefined,
35 | });
36 |
37 | export const AiModalContextProvider = ({
38 | children,
39 | }: {
40 | children: ReactNode;
41 | }) => {
42 | const [selectionRange, setSelectionRange] = useState<{
43 | from: number;
44 | to: number;
45 | }>({
46 | from: 0,
47 | to: 0,
48 | });
49 | const [selection, setSelection] = useState(null);
50 | const [suggestion, setSuggestion] = useState(null);
51 | const [mode, setMode] = useState(null);
52 | const [editor, setEditor] = useState(null);
53 |
54 | return (
55 |
69 | {children}
70 |
71 | );
72 | };
73 |
74 | export const useAiModalContext = () => {
75 | return useContext(AiModalContext);
76 | };
77 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/clsnm.ts:
--------------------------------------------------------------------------------
1 | export function clsnm(
2 | ...classes: (string | boolean | null | undefined)[]
3 | ): string {
4 | const mainClassArr: string[] = [];
5 | const clsArray = [...classes];
6 |
7 | for (const cls of clsArray) {
8 | if (typeof cls === "string" && cls.trim() !== "") {
9 | mainClassArr.push(cls);
10 | }
11 | }
12 |
13 | return mainClassArr.join(" ");
14 | }
15 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/customWallets.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Chain,
3 | Wallet,
4 | getWalletConnectConnector,
5 | } from "@rainbow-me/rainbowkit";
6 |
7 | export interface MyWalletOptions {
8 | projectId: string;
9 | chains: Chain[];
10 | }
11 |
12 | export const claveWallet = ({
13 | chains,
14 | projectId,
15 | }: MyWalletOptions): Wallet => ({
16 | id: "clave-wallet",
17 | name: "Clave",
18 | iconUrl: "https://i.imgur.com/231t7i8.png",
19 | iconBackground: "#000",
20 | downloadUrls: {},
21 | createConnector: () => {
22 | const connector = getWalletConnectConnector({ projectId, chains });
23 |
24 | return {
25 | connector,
26 | mobile: {
27 | getUri: async () => {
28 | const provider = await connector.getProvider();
29 | const uri = await new Promise((resolve) => {
30 | provider.once("display_uri", resolve);
31 | });
32 | return `clave://wc/${uri}`;
33 | },
34 | },
35 | qrCode: {
36 | getUri: async () => {
37 | const provider = await connector.getProvider();
38 | const uri = await new Promise((resolve) =>
39 | provider.once("display_uri", resolve)
40 | );
41 | return `clave://${uri}`;
42 | },
43 | },
44 | };
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/formatAddress.ts:
--------------------------------------------------------------------------------
1 | export const formatAddress = (address: string, pad = 5) => {
2 | return (
3 | address?.substring?.(0, pad) +
4 | "..." +
5 | address?.substring?.(address?.length - pad)
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/formatRPCErrorMessage.ts:
--------------------------------------------------------------------------------
1 | export const formatRpcErrorMessage = (err?: any): string => {
2 | if (err == undefined) return "Unknown error";
3 |
4 | try {
5 | const message = err.error.data.message;
6 | if (message != null && typeof message === "string") {
7 | const split = message.split("execution reverted:");
8 | if (split == undefined || split.length < 2) {
9 | return message;
10 | } else {
11 | return split[1];
12 | }
13 | }
14 | return "Unknown error";
15 | } catch {
16 | return "Unknown error";
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/getAvatar.ts:
--------------------------------------------------------------------------------
1 | import { createAvatar } from "@dicebear/avatars";
2 | import * as style from "@dicebear/avatars-human-sprites";
3 |
4 | export const getUserAvatar = (address: string, size = 128) => {
5 | return createAvatar(style, {
6 | seed: address,
7 | size: size,
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/getPolybaseUserName.ts:
--------------------------------------------------------------------------------
1 | import { PolybaseUser } from "restapi/types";
2 |
3 | export const getPolybaseUserName = (polybaseUser: PolybaseUser | null) => {
4 | return polybaseUser?.name || "Unknown name";
5 | };
6 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/getSharedMessage.ts:
--------------------------------------------------------------------------------
1 | export const getSharedMessage = (noteId: string) => {
2 | return `Here is a shared link Polynote:
3 |
4 | ${window.location.origin}/shared/${noteId}
5 | `;
6 | };
7 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/getToken.ts:
--------------------------------------------------------------------------------
1 | import { ACCESS_TOKEN_KEY } from "consts/storage";
2 |
3 | export const getToken = () => {
4 | return `Bearer ${localStorage.getItem(ACCESS_TOKEN_KEY)}`;
5 | };
6 |
--------------------------------------------------------------------------------
/polynote-frontend/utils/signature.ts:
--------------------------------------------------------------------------------
1 | import { POLYNOTE_CONTRACT } from "consts/contracts";
2 |
3 | export const DOMAIN = {
4 | name: "Polynote",
5 | version: "1",
6 | verifyingContract: POLYNOTE_CONTRACT,
7 | } as const;
8 |
9 | // The named list of all type definitions
10 | export const TYPES = {
11 | User: [
12 | { name: "address", type: "address" },
13 | { name: "message", type: "string" },
14 | ],
15 | } as const;
16 |
17 | export const getSignatureValue = (address: `0x${string}`, message: string) => {
18 | return {
19 | address,
20 | message,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/.env.example:
--------------------------------------------------------------------------------
1 | WALLET_PRIVATE_KEY=
2 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .vscode
9 |
10 | # hardhat artifacts
11 | artifacts
12 | cache
13 |
14 | # zksync artifacts
15 | artifacts-zk
16 | cache-zk
17 |
18 | # Diagnostic reports (https://nodejs.org/api/report.html)
19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
20 |
21 | # Runtime data
22 | pids
23 | *.pid
24 | *.seed
25 | *.pid.lock
26 |
27 | # Directory for instrumented libs generated by jscoverage/JSCover
28 | lib-cov
29 |
30 | # Coverage directory used by tools like istanbul
31 | coverage
32 | *.lcov
33 |
34 | # nyc test coverage
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 | .grunt
39 |
40 | # Bower dependency directory (https://bower.io/)
41 | bower_components
42 |
43 | # node-waf configuration
44 | .lock-wscript
45 |
46 | # Compiled binary addons (https://nodejs.org/api/addons.html)
47 | build/Release
48 |
49 | # Dependency directories
50 | node_modules/
51 | jspm_packages/
52 |
53 | # TypeScript v1 declaration files
54 | typings/
55 |
56 | # TypeScript cache
57 | *.tsbuildinfo
58 |
59 | # Optional npm cache directory
60 | .npm
61 |
62 | # Optional eslint cache
63 | .eslintcache
64 |
65 | # Microbundle cache
66 | .rpt2_cache/
67 | .rts2_cache_cjs/
68 | .rts2_cache_es/
69 | .rts2_cache_umd/
70 |
71 | # Optional REPL history
72 | .node_repl_history
73 |
74 | # Output of 'npm pack'
75 | *.tgz
76 |
77 | # Yarn Integrity file
78 | .yarn-integrity
79 |
80 | # dotenv environment variables file
81 | .env
82 | .env.test
83 |
84 | # parcel-bundler cache (https://parceljs.org/)
85 | .cache
86 |
87 | # Next.js build output
88 | .next
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # Serverless directories
104 | .serverless/
105 |
106 | # FuseBox cache
107 | .fusebox/
108 |
109 | # DynamoDB Local files
110 | .dynamodb/
111 |
112 | # TernJS port file
113 | .tern-port
114 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Matter Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/README.md:
--------------------------------------------------------------------------------
1 | # zkSync Hardhat project template
2 |
3 | This project was scaffolded with [zksync-cli](https://github.com/matter-labs/zksync-cli).
4 |
5 | ## Project Layout
6 |
7 | - `/contracts`: Contains solidity smart contracts.
8 | - `/deploy`: Scripts for contract deployment and interaction.
9 | - `/test`: Test files.
10 | - `hardhat.config.ts`: Configuration settings.
11 |
12 | ## How to Use
13 |
14 | - `npm run compile`: Compiles contracts.
15 | - `npm run deploy`: Deploys using script `/deploy/deploy.ts`.
16 | - `npm run interact`: Interacts with the deployed contract using `/deploy/interact.ts`.
17 | - `npm run test`: Tests the contracts.
18 |
19 | Note: Both `npm run deploy` and `npm run interact` are set in the `package.json`. You can also run your files directly, for example: `npx hardhat deploy-zksync --script deploy.ts`
20 |
21 | ### Environment Settings
22 |
23 | To keep private keys safe, this project pulls in environment variables from `.env` files. Primarily, it fetches the wallet's private key.
24 |
25 | Rename `.env.example` to `.env` and fill in your private key:
26 |
27 | ```
28 | WALLET_PRIVATE_KEY=your_private_key_here...
29 | ```
30 |
31 | ### Network Support
32 |
33 | `hardhat.config.ts` comes with a list of networks to deploy and test contracts. Add more by adjusting the `networks` section in the `hardhat.config.ts`. To make a network the default, set the `defaultNetwork` to its name. You can also override the default using the `--network` option, like: `hardhat test --network dockerizedNode`.
34 |
35 | ### Local Tests
36 |
37 | Running `npm run test` by default runs the [zkSync In-memory Node](https://era.zksync.io/docs/tools/testing/era-test-node.html) provided by the [@matterlabs/hardhat-zksync-node](https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-node.html) tool.
38 |
39 | Important: zkSync In-memory Node currently supports only the L2 node. If contracts also need L1, use another testing environment like Dockerized Node. Refer to [test documentation](https://era.zksync.io/docs/tools/testing/) for details.
40 |
41 | ## Useful Links
42 |
43 | - [Docs](https://era.zksync.io/docs/dev/)
44 | - [Official Site](https://zksync.io/)
45 | - [GitHub](https://github.com/matter-labs)
46 | - [Twitter](https://twitter.com/zksync)
47 | - [Discord](https://join.zksync.dev/)
48 |
49 | ## License
50 |
51 | This project is under the [MIT](./LICENSE) license.
--------------------------------------------------------------------------------
/polynote-zksync-contracts/contracts/Polynote.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.0;
3 |
4 | contract Polynote {
5 | mapping(address => mapping(string => address[])) sharedAddresses;
6 |
7 | event Shared(address owner, string noteId, address partner);
8 | event Unshared(address owner, string noteId, address partner);
9 |
10 | function setPartners(
11 | string memory _noteId,
12 | address[] memory _partners
13 | ) public {
14 | sharedAddresses[msg.sender][_noteId] = _partners;
15 | for (uint256 i = 0; i < _partners.length; ++i) {
16 | emit Shared(msg.sender, _noteId, _partners[i]);
17 | }
18 | }
19 |
20 | function isShared(
21 | address _owner,
22 | string memory _noteId,
23 | address _partner
24 | ) public view returns (bool) {
25 | bool shared;
26 | for (uint i = 0; i < sharedAddresses[_owner][_noteId].length; ++i) {
27 | if (sharedAddresses[_owner][_noteId][i] == _partner) {
28 | shared = true;
29 | }
30 | }
31 | return shared;
32 | }
33 |
34 | function getSharedAddresses(
35 | address _owner,
36 | string memory _noteId
37 | ) public view returns (address[] memory) {
38 | return sharedAddresses[_owner][_noteId];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/deploy/deploy.ts:
--------------------------------------------------------------------------------
1 | import { deployContract } from "./utils";
2 |
3 | // An example of a basic deploy script
4 | // It will deploy a Polynote contract to selected network
5 | // as well as verify it on Block Explorer if possible for the network
6 | export default async function () {
7 | const contractArtifactName = "Polynote";
8 | const constructorArguments = [];
9 | await deployContract(contractArtifactName, constructorArguments);
10 | }
11 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import { HardhatUserConfig } from "hardhat/config";
2 |
3 | import "@matterlabs/hardhat-zksync-node";
4 | import "@matterlabs/hardhat-zksync-deploy";
5 | import "@matterlabs/hardhat-zksync-solc";
6 | import "@matterlabs/hardhat-zksync-verify";
7 |
8 | const config: HardhatUserConfig = {
9 | defaultNetwork: "zkSyncSepoliaTestnet",
10 | networks: {
11 | zkSyncSepoliaTestnet: {
12 | url: "https://sepolia.era.zksync.dev",
13 | ethNetwork: "sepolia",
14 | zksync: true,
15 | verifyURL:
16 | "https://explorer.sepolia.era.zksync.dev/contract_verification",
17 | },
18 | // zkSyncMainnet: {
19 | // url: "https://mainnet.era.zksync.io",
20 | // ethNetwork: "mainnet",
21 | // zksync: true,
22 | // verifyURL:
23 | // "https://zksync2-mainnet-explorer.zksync.io/contract_verification",
24 | // },
25 | // zkSyncGoerliTestnet: {
26 | // // deprecated network
27 | // url: "https://testnet.era.zksync.dev",
28 | // ethNetwork: "goerli",
29 | // zksync: true,
30 | // verifyURL:
31 | // "https://zksync2-testnet-explorer.zksync.dev/contract_verification",
32 | // },
33 | // dockerizedNode: {
34 | // url: "http://localhost:3050",
35 | // ethNetwork: "http://localhost:8545",
36 | // zksync: true,
37 | // },
38 | // inMemoryNode: {
39 | // url: "http://127.0.0.1:8011",
40 | // ethNetwork: "", // in-memory node doesn't support eth node; removing this line will cause an error
41 | // zksync: true,
42 | // },
43 | hardhat: {
44 | zksync: true,
45 | },
46 | },
47 | zksolc: {
48 | version: "latest",
49 | settings: {
50 | // find all available options in the official documentation
51 | // https://era.zksync.io/docs/tools/hardhat/hardhat-zksync-solc.html#configuration
52 | },
53 | },
54 | solidity: {
55 | version: "0.8.17",
56 | },
57 | };
58 |
59 | export default config;
60 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polynote-zksync-contracts",
3 | "description": "Zksync contracts for Polynote",
4 | "private": true,
5 | "author": "Farhad Asgarov",
6 | "license": "MIT",
7 | "repository": "https://github.com/itublockchain/polynote-scaling2023.git",
8 | "scripts": {
9 | "deploy": "hardhat deploy-zksync --script deploy.ts",
10 | "compile": "hardhat compile",
11 | "clean": "hardhat clean",
12 | "test": "hardhat test --network hardhat"
13 | },
14 | "devDependencies": {
15 | "@matterlabs/hardhat-zksync-deploy": "^1.1.2",
16 | "@matterlabs/hardhat-zksync-node": "^1.0.1",
17 | "@matterlabs/hardhat-zksync-solc": "^1.0.6",
18 | "@matterlabs/hardhat-zksync-verify": "^1.2.2",
19 | "@matterlabs/zksync-contracts": "^0.6.1",
20 | "@nomiclabs/hardhat-etherscan": "^3.1.7",
21 | "@openzeppelin/contracts": "^4.6.0",
22 | "@types/chai": "^4.3.4",
23 | "@types/mocha": "^10.0.1",
24 | "chai": "^4.3.7",
25 | "dotenv": "^16.0.3",
26 | "ethers": "^6.9.2",
27 | "hardhat": "^2.12.4",
28 | "mocha": "^10.2.0",
29 | "ts-node": "^10.9.1",
30 | "typescript": "^4.9.5",
31 | "zksync-ethers": "^6.0.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/test/erc20/myerc20token.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { Contract, Wallet } from "zksync-ethers";
3 | import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../../deploy/utils';
4 | import * as ethers from "ethers";
5 |
6 | describe("MyERC20Token", function () {
7 | let tokenContract: Contract;
8 | let ownerWallet: Wallet;
9 | let userWallet: Wallet;
10 |
11 | before(async function () {
12 | ownerWallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey);
13 | userWallet = getWallet(LOCAL_RICH_WALLETS[1].privateKey);
14 |
15 | tokenContract = await deployContract("MyERC20Token", [], { wallet: ownerWallet, silent: true });
16 | });
17 |
18 | it("Should have correct initial supply", async function () {
19 | const initialSupply = await tokenContract.totalSupply();
20 | expect(initialSupply).to.equal(BigInt("1000000000000000000000000")); // 1 million tokens with 18 decimals
21 | });
22 |
23 | it("Should allow owner to burn tokens", async function () {
24 | const burnAmount = ethers.parseEther("10"); // Burn 10 tokens
25 | const tx = await tokenContract.burn(burnAmount);
26 | await tx.wait();
27 | const afterBurnSupply = await tokenContract.totalSupply();
28 | expect(afterBurnSupply).to.equal(BigInt("999990000000000000000000")); // 999,990 tokens remaining
29 | });
30 |
31 | it("Should allow user to transfer tokens", async function () {
32 | const transferAmount = ethers.parseEther("50"); // Transfer 50 tokens
33 | const tx = await tokenContract.transfer(userWallet.address, transferAmount);
34 | await tx.wait();
35 | const userBalance = await tokenContract.balanceOf(userWallet.address);
36 | expect(userBalance).to.equal(transferAmount);
37 | });
38 |
39 | it("Should fail when user tries to burn more tokens than they have", async function () {
40 | const userTokenContract = new Contract(await tokenContract.getAddress(), tokenContract.interface, userWallet);
41 | const burnAmount = ethers.parseEther("100"); // Try to burn 100 tokens
42 | try {
43 | await userTokenContract.burn(burnAmount);
44 | expect.fail("Expected burn to revert, but it didn't");
45 | } catch (error) {
46 | expect(error.message).to.include("burn amount exceeds balance");
47 | }
48 | });
49 | });
50 |
51 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/test/greeter.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../deploy/utils';
3 |
4 | describe('Greeter', function () {
5 | it("Should return the new greeting once it's changed", async function () {
6 | const wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey);
7 |
8 | const greeting = "Hello world!";
9 | const greeter = await deployContract("Greeter", [greeting], { wallet, silent: true });
10 |
11 | expect(await greeter.greet()).to.eq(greeting);
12 |
13 | const newGreeting = "Hola, mundo!";
14 | const setGreetingTx = await greeter.setGreeting(newGreeting);
15 |
16 | // wait until the transaction is processed
17 | await setGreetingTx.wait();
18 |
19 | expect(await greeter.greet()).to.equal(newGreeting);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/polynote-zksync-contracts/test/nft/mynft.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { Contract, Wallet } from "zksync-ethers";
3 | import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../../deploy/utils';
4 |
5 | describe("MyNFT", function () {
6 | let nftContract: Contract;
7 | let ownerWallet: Wallet;
8 | let recipientWallet: Wallet;
9 |
10 | before(async function () {
11 | ownerWallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey);
12 | recipientWallet = getWallet(LOCAL_RICH_WALLETS[1].privateKey);
13 |
14 | nftContract = await deployContract(
15 | "MyNFT",
16 | ["MyNFTName", "MNFT", "https://mybaseuri.com/token/"],
17 | { wallet: ownerWallet, silent: true }
18 | );
19 | });
20 |
21 | it("Should mint a new NFT to the recipient", async function () {
22 | const tx = await nftContract.mint(recipientWallet.address);
23 | await tx.wait();
24 | const balance = await nftContract.balanceOf(recipientWallet.address);
25 | expect(balance).to.equal(BigInt("1"));
26 | });
27 |
28 | it("Should have correct token URI after minting", async function () {
29 | const tokenId = 1; // Assuming the first token minted has ID 1
30 | const tokenURI = await nftContract.tokenURI(tokenId);
31 | expect(tokenURI).to.equal("https://mybaseuri.com/token/1");
32 | });
33 |
34 | it("Should allow owner to mint multiple NFTs", async function () {
35 | const tx1 = await nftContract.mint(recipientWallet.address);
36 | await tx1.wait();
37 | const tx2 = await nftContract.mint(recipientWallet.address);
38 | await tx2.wait();
39 | const balance = await nftContract.balanceOf(recipientWallet.address);
40 | expect(balance).to.equal(BigInt("3")); // 1 initial nft + 2 minted
41 | });
42 |
43 | it("Should not allow non-owner to mint NFTs", async function () {
44 | try {
45 | const tx3 = await (nftContract.connect(recipientWallet) as Contract).mint(recipientWallet.address);
46 | await tx3.wait();
47 | expect.fail("Expected mint to revert, but it didn't");
48 | } catch (error) {
49 | expect(error.message).to.include("Ownable: caller is not the owner");
50 | }
51 | });
52 | });
53 |
--------------------------------------------------------------------------------