├── .asdfrc ├── .eslintignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── chore.md │ ├── custom.md │ └── feature_request.md └── workflows │ ├── ci-cd.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json.example └── settings.json.example ├── LICENSE ├── README.md ├── babel.config.json ├── netlify.toml ├── package-lock.json ├── package.json ├── sass ├── _animation.scss ├── _base.scss ├── _color.scss ├── _font.scss ├── _mixin.scss └── _size.scss ├── src ├── app │ └── static │ │ ├── discord.svg │ │ ├── favicon.ico │ │ ├── github.svg │ │ ├── logo-black.svg │ │ ├── logo-white.svg │ │ ├── meta-pool │ │ ├── MetaPool_logo.png │ │ ├── MetaPool_logo.svg │ │ └── MetaPool_symbol.svg │ │ ├── mintbase │ │ ├── Mintbase_logo.svg │ │ └── Mintbase_symbol.svg │ │ ├── outline_dynamic_feed_black_24dp.png │ │ ├── parallel.svg │ │ ├── paras │ │ └── Paras_logo.svg │ │ ├── sequence.svg │ │ ├── telegram.svg │ │ ├── token-farm │ │ └── TokenFarm_symbol.png │ │ └── twitter.svg ├── entities │ ├── fungible-token │ │ ├── context.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── ft-format.ts │ │ ├── model │ │ │ └── ft-info.ts │ │ └── ui │ │ │ └── ft-balances.tsx │ ├── index.ts │ ├── job │ │ ├── index.ts │ │ ├── job.lib.ts │ │ ├── job.model.ts │ │ ├── job.service.ts │ │ ├── job.ui.scss │ │ └── job.ui.tsx │ ├── multicall-instance │ │ ├── context.ts │ │ ├── index.ts │ │ ├── model │ │ │ ├── mi-admins.ts │ │ │ ├── mi-settings.ts │ │ │ └── mi-tokens.ts │ │ └── ui │ │ │ ├── mi-admin.tsx │ │ │ ├── mi-admins.tsx │ │ │ ├── mi-token-whitelist.tsx │ │ │ └── mi-whitelisted-token.tsx │ ├── near-token │ │ ├── context.ts │ │ ├── index.ts │ │ ├── model │ │ │ └── near-balances.ts │ │ └── ui │ │ │ └── nt-balances.tsx │ ├── task │ │ ├── config │ │ │ ├── initial-data.ts │ │ │ └── keywords.ts │ │ ├── index.ts │ │ └── ui │ │ │ ├── task.jsx │ │ │ └── task.scss │ └── wallet │ │ ├── index.ts │ │ └── ui │ │ ├── providers.tsx │ │ ├── wallet.scss │ │ └── wallet.tsx ├── families │ ├── base.scss │ ├── base.tsx │ ├── custom.scss │ ├── custom.tsx │ ├── families.ts │ ├── index.ts │ ├── meta-pool │ │ ├── family.ts │ │ ├── meta-pool.scss │ │ ├── nslp-add-liquidity.tsx │ │ └── nslp-remove-liquidity.tsx │ ├── mintbase │ │ ├── add-minter.tsx │ │ ├── buy-nft.tsx │ │ ├── create-store.tsx │ │ ├── family.ts │ │ ├── mintbase.scss │ │ ├── remove-minter.tsx │ │ └── transfer-store-ownership.tsx │ ├── multicall │ │ ├── family.ts │ │ ├── multicall.scss │ │ └── transfer.tsx │ ├── near │ │ ├── deposit-and-stake.tsx │ │ ├── family.ts │ │ ├── ft-transfer-call.tsx │ │ ├── ft-transfer.tsx │ │ ├── mft-transfer-call.tsx │ │ ├── mft-transfer.tsx │ │ ├── near.scss │ │ ├── nft-approve.tsx │ │ ├── nft-revoke.tsx │ │ ├── nft-transfer-call.tsx │ │ ├── nft-transfer.tsx │ │ ├── storage-deposit.tsx │ │ ├── storage-unregister.tsx │ │ ├── storage-withdraw.tsx │ │ ├── unstake.tsx │ │ ├── unwrap-near.tsx │ │ ├── withdraw.tsx │ │ └── wrap-near.tsx │ ├── paras │ │ ├── buy-nft.tsx │ │ ├── family.ts │ │ └── paras.scss │ └── token-farm │ │ ├── create-token.tsx │ │ ├── family.ts │ │ └── token-farm.scss ├── features │ ├── external-login │ │ ├── context.ts │ │ ├── index.ts │ │ ├── model │ │ │ └── el-dialogs.ts │ │ └── ui │ │ │ ├── el-dialog.scss │ │ │ ├── el-dialogs.tsx │ │ │ └── el-menu.tsx │ ├── index.ts │ ├── scheduling │ │ └── settings-change │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── ui │ │ │ └── scheduling-settings-form.tsx │ └── tokens │ │ └── whitelist-change │ │ ├── context.ts │ │ ├── index.ts │ │ └── ui │ │ └── tokens-whitelist-form.tsx ├── global.scss ├── index.html ├── index.tsx ├── near-config.ts ├── pages │ ├── app │ │ ├── app.jsx │ │ ├── app.scss │ │ └── index.ts │ └── dao │ │ ├── dao.scss │ │ ├── dao.tsx │ │ ├── funds │ │ ├── funds.scss │ │ └── funds.tsx │ │ ├── index.ts │ │ ├── jobs │ │ ├── jobs.scss │ │ └── jobs.tsx │ │ └── settings │ │ ├── settings.scss │ │ └── settings.tsx ├── shared │ ├── lib │ │ ├── args-old.ts │ │ ├── args │ │ │ ├── args-error.ts │ │ │ ├── args-types │ │ │ │ ├── args-array.ts │ │ │ │ ├── args-big.ts │ │ │ │ ├── args-boolean.ts │ │ │ │ ├── args-mixed.ts │ │ │ │ ├── args-number.ts │ │ │ │ ├── args-object.ts │ │ │ │ └── args-string.ts │ │ │ └── args.ts │ │ ├── call.ts │ │ ├── contracts │ │ │ ├── generic.ts │ │ │ ├── meta-pool.ts │ │ │ ├── mintbase.ts │ │ │ ├── multicall.ts │ │ │ ├── paras.ts │ │ │ ├── sputnik-dao.ts │ │ │ ├── staking-pool.ts │ │ │ └── token-farm.ts │ │ ├── converter.ts │ │ ├── loader.ts │ │ ├── persistent.ts │ │ ├── props.ts │ │ ├── standards │ │ │ ├── fungibleToken.ts │ │ │ ├── multiFungibleToken.ts │ │ │ ├── nonFungibleToken.ts │ │ │ └── storageManagement.ts │ │ ├── types.d.ts │ │ ├── validation.ts │ │ ├── wallet.ts │ │ └── window.ts │ └── ui │ │ ├── design │ │ ├── button-group │ │ │ ├── button-group.scss │ │ │ ├── button-group.tsx │ │ │ └── index.ts │ │ ├── button │ │ │ ├── button.scss │ │ │ ├── button.tsx │ │ │ └── index.ts │ │ ├── context.ts │ │ ├── data-inspector │ │ │ ├── context.ts │ │ │ ├── data-inspector-node.tsx │ │ │ ├── data-inspector.scss │ │ │ ├── data-inspector.tsx │ │ │ └── index.ts │ │ ├── date-time-picker │ │ │ ├── date-time-picker.scss │ │ │ ├── date-time-picker.tsx │ │ │ └── index.ts │ │ ├── dialog │ │ │ ├── dialog.scss │ │ │ ├── dialog.tsx │ │ │ └── index.ts │ │ ├── icon-label │ │ │ ├── icon-label.scss │ │ │ ├── icon-label.tsx │ │ │ └── index.ts │ │ ├── icons │ │ │ ├── index.ts │ │ │ └── near │ │ │ │ ├── near.scss │ │ │ │ └── near.tsx │ │ ├── index.ts │ │ ├── link │ │ │ ├── index.ts │ │ │ ├── link.scss │ │ │ └── link.tsx │ │ ├── near-link │ │ │ ├── index.ts │ │ │ ├── near-link.scss │ │ │ └── near-link.tsx │ │ ├── placeholder │ │ │ ├── index.ts │ │ │ ├── placeholder-content.scss │ │ │ ├── placeholder-content.tsx │ │ │ ├── placeholder.scss │ │ │ └── placeholder.tsx │ │ ├── popup-menu │ │ │ ├── index.ts │ │ │ ├── popup-menu.scss │ │ │ └── popup-menu.tsx │ │ ├── scrollable │ │ │ ├── index.ts │ │ │ ├── scrollable.scss │ │ │ └── scrollable.tsx │ │ ├── table │ │ │ ├── index.ts │ │ │ ├── row.scss │ │ │ ├── row.tsx │ │ │ ├── table.scss │ │ │ └── table.tsx │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── item.scss │ │ │ ├── item.tsx │ │ │ ├── layout.scss │ │ │ ├── layout.tsx │ │ │ └── tabs.tsx │ │ ├── text-input │ │ │ ├── index.ts │ │ │ └── text-input.tsx │ │ ├── tile │ │ │ ├── index.ts │ │ │ ├── tile.scss │ │ │ └── tile.tsx │ │ └── tooltip │ │ │ ├── index.ts │ │ │ ├── tooltip.scss │ │ │ └── tooltip.tsx │ │ └── form │ │ ├── elements │ │ ├── form-control.tsx │ │ ├── form-label.scss │ │ ├── form-label.tsx │ │ └── form-radio-group.tsx │ │ ├── fields │ │ ├── checkbox-field.scss │ │ ├── checkbox-field.tsx │ │ ├── choice-field.scss │ │ ├── choice-field.tsx │ │ ├── file-field.scss │ │ ├── file-field.tsx │ │ ├── form-radio.scss │ │ ├── form-radio.tsx │ │ ├── info-field.scss │ │ ├── info-field.tsx │ │ ├── select-field.tsx │ │ ├── text-field.scss │ │ ├── text-field.tsx │ │ ├── unit-field.scss │ │ └── unit-field.tsx │ │ └── index.ts ├── types │ └── images.d.ts └── widgets │ ├── builder │ ├── builder.jsx │ ├── builder.scss │ └── index.ts │ ├── column │ ├── column.jsx │ ├── column.scss │ └── index.ts │ ├── dialogs-layer │ ├── index.ts │ └── ui │ │ └── dialogs-layer.tsx │ ├── editor │ ├── editor.jsx │ ├── editor.scss │ └── index.ts │ ├── export │ ├── export.scss │ ├── export.tsx │ └── index.ts │ ├── index.ts │ ├── menu │ ├── index.ts │ ├── menu.jsx │ └── menu.scss │ ├── settings-editor │ ├── context.ts │ ├── index.ts │ └── ui │ │ ├── se-proposal-form.scss │ │ ├── se-proposal-form.tsx │ │ ├── settings-editor.scss │ │ └── settings-editor.tsx │ ├── sidebar │ ├── dialogs.jsx │ ├── index.ts │ ├── sidebar.jsx │ └── sidebar.scss │ └── token-balances │ ├── context.ts │ ├── index.ts │ └── ui │ ├── token-balances.scss │ └── token-balances.tsx └── tsconfig.json /.asdfrc: -------------------------------------------------------------------------------- 1 | # Needed in order to support .nvmrc when using asdf 2 | # See https://github.com/asdf-vm/asdf-nodejs#nvmrc-and-node-version-support 3 | legacy_version_file = yes 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Take note of a to-do that has to be addressed 4 | title: '' 5 | labels: 'chore' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What needs to be done?** 11 | Summarize the task and any sub tasks. 12 | 13 | - [ ] clean up ... 14 | - [ ] move ... to new file 15 | - [ ] adjust any imports 16 | 17 | **Additional context** 18 | Add any other context about the chore here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yaml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CI/CD" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | # Allows you to run this workflow manually from the Actions tab 21 | workflow_dispatch: 22 | 23 | env: 24 | # build & deploy gh-pages for testnet 25 | NEAR_ENV: testnet 26 | 27 | jobs: 28 | test-deploy: 29 | name: test and deploy 30 | runs-on: ubuntu-latest 31 | permissions: 32 | security-events: write 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup Node.js v16 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: '16' 41 | 42 | # install dependencies and apply patches (if any exist) 43 | - name: install dependencies 44 | run: npm install 45 | 46 | # check for typescript errors 47 | - name: typecheck 48 | run: npm run typecheck 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v2 53 | with: 54 | languages: javascript 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually. 58 | - name: CodeQL Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v2 63 | 64 | - name: Build 65 | run: npm run build 66 | 67 | # Deploy, only runs on "push" event on main branch 68 | - name: Deploy 69 | uses: peaceiris/actions-gh-pages@v3 70 | if: ${{ github.event_name == 'push' }} 71 | with: 72 | deploy_key: ${{ secrets.GH_PAGES_DEPLOYER }} 73 | publish_dir: ./dist 74 | publish_branch: gh-pages 75 | cname: testnet.multicall.app 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | schedule: 18 | - cron: '18 15 * * 3' 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'javascript' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 34 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | 49 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 50 | # queries: security-extended,security-and-quality 51 | 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # If the Autobuild fails above, remove it and uncomment the following three lines. 62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 63 | 64 | # - run: | 65 | # echo "Run, Build Application using script" 66 | # ./location_of_script_within_repo/buildscript.sh 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v2 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # Developer note: near.gitignore will be renamed to .gitignore upon project creation 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # build 9 | /out 10 | /dist 11 | /.parcel-cache/ 12 | 13 | # keys 14 | /neardev 15 | 16 | # testing 17 | /coverage 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | /.cache 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | # vscode config 35 | .vscode/settings.json 36 | .vscode/extensions.json 37 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | # other commands, like tests, go here 6 | 7 | npm run typecheck 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.17.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 120, 4 | "semi": true, 5 | "singleQuote": false, 6 | "tabWidth": 4, 7 | "trailingComma": "es5", 8 | "useTabs": false, 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "singleAttributePerLine": true, 12 | "htmlWhitespaceSensitivity": "strict" 13 | } -------------------------------------------------------------------------------- /.vscode/extensions.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.format.enable": true, 3 | "mergeEditor.diffAlgorithm": "experimental", 4 | "[javascript]": { 5 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 6 | "editor.formatOnSave": true 7 | }, 8 | "[javascriptreact]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | "editor.formatOnSave": true 11 | }, 12 | "[typescript]": { 13 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 14 | "editor.formatOnSave": true 15 | }, 16 | "[typescriptreact]": { 17 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 18 | "editor.formatOnSave": true 19 | }, 20 | "[scss]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode", 22 | "editor.formatOnSave": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 near-multicall 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEAR Multicall 2 | 3 | Multicall is a tool for DAOs that allows proposal creators to bundle multiple cross-contract calls in one proposal. 4 | You may find more information on the project [here](https://github.com/near-multicall/contracts). 5 | 6 | --- 7 | 8 | ## Multicall UI 9 | 10 | This repository contains graphical user interface application for building [multicalls](https://github.com/near-multicall/contracts). 11 | Try it out at [NEAR Mainnet](https://multicall.app) or [NEAR Testnet](https://testnet.multicall.app) 12 | 13 | --- 14 | 15 | ### Development 16 | 17 | #### Quick start 18 | 19 | ##### Install dependencies 20 | 21 | ```sh 22 | npm install 23 | ``` 24 | 25 | ##### Run development server 26 | 27 | For **NEAR Testnet**: 28 | 29 | ```sh 30 | npm run start:testnet 31 | ``` 32 | 33 | For **NEAR Mainnet**: 34 | 35 | ```sh 36 | npm run start:mainnet 37 | ``` 38 | 39 | #### Architectural specification 40 | 41 | This application's architecture applies [Feature-Sliced Design](https://Feature-Sliced.Design/) principles. 42 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [], 3 | "plugins": [ 4 | ["@babel/transform-runtime"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # Deploy Preview context: All Deploy Previews 2 | # will inherit these settings. 3 | [context.deploy-preview] 4 | command = "npm run build:testnet" 5 | 6 | [[plugins]] 7 | package = "@netlify/plugin-lighthouse" 8 | 9 | # optional, fails build when a category is below a threshold 10 | [plugins.inputs.thresholds] 11 | # temporarily disable performance bound 12 | # performance = 0.5 13 | accessibility = 0.8 14 | best-practices = 0.8 15 | seo = 0.7 16 | pwa = 0 17 | 18 | # optional, deploy the lighthouse report to a path under your site 19 | [plugins.inputs] 20 | output_path = "reports/lighthouse.html" 21 | -------------------------------------------------------------------------------- /sass/_animation.scss: -------------------------------------------------------------------------------- 1 | $menu-expand-width: 50%; 2 | $menu-expand-time: 200ms; 3 | -------------------------------------------------------------------------------- /sass/_base.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/font"; 4 | @use "sass/mixin"; 5 | 6 | .light-textfield { 7 | &.MuiTextField-root { 8 | .MuiInputLabel-root, 9 | .MuiFilledInput-root.Mui-focused { 10 | &:not(.Mui-error) { 11 | color: color.$light-text !important; 12 | } 13 | &.Mui-error { 14 | color: color.$red !important; 15 | } 16 | } 17 | .MuiFilledInput-root, 18 | .MuiFilledInput-root:hover { 19 | background-color: rgba(color.$white, 0.06) !important; 20 | &::before, 21 | &:hover::before { 22 | border-bottom: 1px solid rgba(color.$white, 0.42); 23 | } 24 | &::after { 25 | border-bottom: 2px solid color.$white; 26 | } 27 | &.Mui-error { 28 | &::before, 29 | &:hover::before { 30 | border-bottom: 1px solid rgba(color.$red, 0.42) !important; 31 | } 32 | &::after { 33 | border-bottom: 2px solid color.$red !important; 34 | } 35 | } 36 | input, 37 | textarea { 38 | color: color.$white; 39 | font-size: size.$text; 40 | } 41 | textarea { 42 | @include mixin.no-scrollbar; 43 | white-space: pre; 44 | overflow-x: scroll !important; 45 | } 46 | } 47 | *:not(input) { 48 | color: color.$light-text; 49 | font-size: 1em; 50 | } 51 | font-size: 1.8rem; 52 | } 53 | } 54 | 55 | .spacer { 56 | flex: 1; 57 | } 58 | 59 | .loader { 60 | display: flex; 61 | flex: 1; 62 | align-self: center; 63 | align-items: center; 64 | justify-content: center; 65 | width: 100%; 66 | height: 100%; 67 | 68 | &::after { 69 | content: " "; 70 | display: block; 71 | width: 64px; 72 | height: 64px; 73 | border-radius: 100%; 74 | border: 6px solid color.$lightish; 75 | border-color: color.$lightish transparent color.$lightish transparent; 76 | animation: lds-dual-ring 1.2s linear infinite; 77 | } 78 | 79 | @keyframes lds-dual-ring { 80 | 0% { 81 | transform: rotate(0deg); 82 | } 83 | 100% { 84 | transform: rotate(360deg); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sass/_color.scss: -------------------------------------------------------------------------------- 1 | $green: #93c99e; 2 | $purple: #d8beda; 3 | $red: #f6a7b2; 4 | $blue: #b2dff2; 5 | $yellow: #ffdf82; 6 | 7 | $lightest: #dcefea; 8 | $light: #b4ccca; 9 | $lightish: #a4bab8; 10 | $darkish: #839595; 11 | $dark: #637171; 12 | $darkest: #424c4e; 13 | 14 | $white: #edfdf9; 15 | $black: #323a3c; 16 | 17 | $text: #2a2a2a; 18 | $light-text: #e0e0e0; 19 | -------------------------------------------------------------------------------- /sass/_font.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Titillium+Web:ital,wght@0,200;0,400;0,600;1,400&display=swap"); 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Icons"); 3 | 4 | $code: "Roboto Mono", monospace; 5 | $text: "Titillium Web", sans-serif; 6 | -------------------------------------------------------------------------------- /sass/_mixin.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/color"; 3 | 4 | @mixin full { 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | @mixin page-wrapper { 13 | position: relative; 14 | height: 100%; 15 | width: calc(100% - size.$sidebar-width); 16 | left: size.$sidebar-width; 17 | &::before { 18 | content: ""; 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | height: calc(2 * size.$gap + 0.5 * size.$large-text); 23 | min-width: 100%; 24 | background-color: color.$light; 25 | } 26 | } 27 | 28 | @mixin available-width { 29 | width: 100%; 30 | width: -moz-available; /* WebKit-based browsers will ignore this. */ 31 | width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ 32 | width: fill-available; 33 | } 34 | 35 | @mixin center-items($x: center, $y: center) { 36 | display: flex; 37 | justify-content: #{$x}; 38 | align-items: #{$y}; 39 | } 40 | 41 | @mixin no-scrollbar { 42 | scrollbar-width: none; 43 | -ms-overflow-style: -ms-autohiding-scrollbar; 44 | &::-webkit-scrollbar { 45 | display: none; 46 | } 47 | } 48 | 49 | @mixin no-wrap { 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | white-space: nowrap; 53 | } 54 | 55 | @mixin popup { 56 | position: fixed; 57 | display: flex; 58 | justify-content: center; 59 | z-index: 2; 60 | gap: size.$gap; 61 | padding: 12.5vh 12.5%; 62 | width: 75%; 63 | height: 75vh; 64 | top: 0; 65 | left: 0; 66 | background-color: rgba(255, 255, 255, 0.87); 67 | button, 68 | .button-container { 69 | position: absolute; 70 | bottom: size.$gap; 71 | right: size.$gap; 72 | } 73 | } 74 | 75 | @mixin icon { 76 | color: color.$lightish; 77 | font-size: size.$large-text; 78 | &:hover, 79 | &:focus { 80 | color: color.$dark; 81 | } 82 | } 83 | 84 | @mixin light-icon { 85 | color: color.$lightish; 86 | font-size: size.$large-text; 87 | &:hover, 88 | &:focus { 89 | color: color.$lightest; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /sass/_size.scss: -------------------------------------------------------------------------------- 1 | $unit: 1.6rem; 2 | $gap: 2.8rem; 3 | 4 | $stroke: 3.2rem; 5 | 6 | $smaller-text: 1.3rem; 7 | $small-text: 1.4rem; 8 | $text: 1.8rem; 9 | $large-text: 2.3rem; 10 | $huge-text: 9.6rem; 11 | 12 | $task-height: 14.2rem; 13 | $task-width: 34rem; 14 | $task-radius: 2.4rem; 15 | 16 | $menu-width: 0rem; // 25vw; 17 | 18 | $Tabs-layout-buttonsPanel-height: 1.4 * $gap; 19 | $sidebar-width: 2 * $gap; 20 | -------------------------------------------------------------------------------- /src/app/static/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-multicall/ui/bef3517bfde757bf65f11aa8a966fffc76a0b2c1/src/app/static/favicon.ico -------------------------------------------------------------------------------- /src/app/static/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/logo-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/logo-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/meta-pool/MetaPool_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-multicall/ui/bef3517bfde757bf65f11aa8a966fffc76a0b2c1/src/app/static/meta-pool/MetaPool_logo.png -------------------------------------------------------------------------------- /src/app/static/meta-pool/MetaPool_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/static/meta-pool/MetaPool_symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/static/mintbase/Mintbase_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/static/mintbase/Mintbase_symbol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/outline_dynamic_feed_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-multicall/ui/bef3517bfde757bf65f11aa8a966fffc76a0b2c1/src/app/static/outline_dynamic_feed_black_24dp.png -------------------------------------------------------------------------------- /src/app/static/telegram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/token-farm/TokenFarm_symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-multicall/ui/bef3517bfde757bf65f11aa8a966fffc76a0b2c1/src/app/static/token-farm/TokenFarm_symbol.png -------------------------------------------------------------------------------- /src/app/static/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/entities/fungible-token/context.ts: -------------------------------------------------------------------------------- 1 | import { Multicall } from "../../shared/lib/contracts/multicall"; 2 | import { SputnikDAO } from "../../shared/lib/contracts/sputnik-dao"; 3 | 4 | export namespace FT { 5 | export interface Inputs { 6 | adapters: { 7 | dao: SputnikDAO; 8 | multicall: Multicall; 9 | }; 10 | } 11 | } 12 | 13 | export class ModuleContext { 14 | static FRACTIONAL_PART_LENGTH = 5; 15 | } 16 | -------------------------------------------------------------------------------- /src/entities/fungible-token/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext, FT as FTModule } from "./context"; 2 | import { ftBalances } from "./ui/ft-balances"; 3 | 4 | export { type FTModule }; 5 | 6 | export class FT extends ModuleContext { 7 | static balances = ftBalances; 8 | } 9 | -------------------------------------------------------------------------------- /src/entities/fungible-token/lib/ft-format.ts: -------------------------------------------------------------------------------- 1 | import { Big, formatTokenAmount } from "../../../shared/lib/converter"; 2 | 3 | import { ModuleContext } from "../context"; 4 | 5 | const amountToDisplayAmount = (amount: string, decimals: number): string => { 6 | const formattedAmount = formatTokenAmount(amount, decimals), 7 | minimalDisplayAmount = Big("10").pow(-ModuleContext.FRACTIONAL_PART_LENGTH).toFixed(); 8 | 9 | return Big(formattedAmount).gt("0") && Big(formattedAmount).lt(minimalDisplayAmount) 10 | ? "< " + minimalDisplayAmount 11 | : formatTokenAmount(amount, decimals, ModuleContext.FRACTIONAL_PART_LENGTH); 12 | }; 13 | 14 | export const FTFormat = { 15 | amountToDisplayAmount, 16 | }; 17 | -------------------------------------------------------------------------------- /src/entities/fungible-token/model/ft-info.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { Big } from "../../../shared/lib/converter"; 4 | import { FungibleToken } from "../../../shared/lib/standards/fungibleToken"; 5 | import { FT } from "../context"; 6 | 7 | type FTInfo = { 8 | data: { metadata: FungibleToken["metadata"]; dao: string; multicall: string; total: string }[] | null; 9 | loading: boolean; 10 | }; 11 | 12 | export class FTInfoModel { 13 | private static readonly nonZeroBalancesFetchFx = async ( 14 | { dao, multicall }: FT.Inputs["adapters"], 15 | callback: (result: FTInfo) => void 16 | ) => { 17 | /* Get LikelyTokens list on DAO and its Multicall instance */ 18 | const [daoLikelyTokensList, multicallLikelyTokensList] = await Promise.all([ 19 | FungibleToken.getLikelyTokenContracts(dao.address), 20 | FungibleToken.getLikelyTokenContracts(multicall.address), 21 | ]); 22 | 23 | /* Merge and de-duplicate both token lists */ 24 | const likelyTokensAddressesList = [...new Set([...daoLikelyTokensList, ...multicallLikelyTokensList])]; 25 | 26 | const likelyTokensList = await Promise.all( 27 | likelyTokensAddressesList.map((address) => FungibleToken.init(address)) 28 | ); 29 | 30 | const rawBalances = await Promise.all( 31 | likelyTokensList 32 | .filter((token) => token.ready === true) 33 | .map(async (token) => { 34 | const [daoRawBalance, multicallRawBalance] = await Promise.all([ 35 | token.ftBalanceOf(dao.address), 36 | token.ftBalanceOf(multicall.address), 37 | ]); 38 | 39 | return { 40 | metadata: token.metadata, 41 | dao: daoRawBalance, 42 | multicall: multicallRawBalance, 43 | total: Big(multicallRawBalance).add(daoRawBalance).toFixed(), 44 | }; 45 | }) 46 | ); 47 | 48 | // remove tokens with 0 total balance 49 | const nonZeroBalances = rawBalances.filter(({ total }) => Big(total).gt("0")); 50 | 51 | return callback({ 52 | data: nonZeroBalances.map(({ dao, metadata, multicall, total }) => ({ metadata, dao, multicall, total })), 53 | loading: false, 54 | }); 55 | }; 56 | 57 | public static readonly useNonZeroBalances = (adapters: FT.Inputs["adapters"]) => { 58 | const [state, stateUpdate] = useState({ data: null, loading: true }); 59 | 60 | useEffect(() => void FTInfoModel.nonZeroBalancesFetchFx(adapters, stateUpdate), [adapters, stateUpdate]); 61 | 62 | return state; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/entities/fungible-token/ui/ft-balances.tsx: -------------------------------------------------------------------------------- 1 | import { IconLabel, NearIcon } from "../../../shared/ui/design"; 2 | import { FTFormat } from "../lib/ft-format"; 3 | import { FTInfoModel } from "../model/ft-info"; 4 | import { FT } from "../context"; 5 | 6 | interface FTBalancesProps extends FT.Inputs {} 7 | 8 | export const ftBalances = ({ adapters }: FTBalancesProps) => { 9 | const { data } = FTInfoModel.useNonZeroBalances(adapters); 10 | 11 | return !data 12 | ? null 13 | : data.map(({ dao, metadata, multicall, total }) => ({ 14 | content: [ 15 | } 17 | label={metadata.symbol} 18 | />, 19 | 20 | FTFormat.amountToDisplayAmount(multicall, metadata.decimals), 21 | FTFormat.amountToDisplayAmount(dao, metadata.decimals), 22 | FTFormat.amountToDisplayAmount(total, metadata.decimals), 23 | ], 24 | 25 | id: metadata.symbol, 26 | })); 27 | }; 28 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export { Task } from "./task"; 2 | export { Job } from "./job"; 3 | export { FT, type FTModule } from "./fungible-token"; 4 | export { MI, type MIModule } from "./multicall-instance"; 5 | export { NEARToken, type NEARTokenModule } from "./near-token"; 6 | export { Wallet } from "./wallet"; 7 | -------------------------------------------------------------------------------- /src/entities/job/index.ts: -------------------------------------------------------------------------------- 1 | import { JobEntriesTable } from "./job.ui"; 2 | 3 | export class Job { 4 | static EntriesTable = JobEntriesTable; 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/job/job.lib.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | import { JobData } from "./job.model"; 3 | 4 | export class JobLib { 5 | /** 6 | * Decodes base64-encoded arguments of for every multicall's FunctionCall 7 | * and returns a new data structure with encoded version replaced with decoded one. 8 | * 9 | * @returns Updated job data structure. 10 | */ 11 | public static readonly toDecoded = ({ id, status, job }: JobData): JobData => ({ 12 | id, 13 | status, 14 | 15 | job: { 16 | ...job, 17 | 18 | multicalls: job.multicalls.map((multicall) => ({ 19 | ...multicall, 20 | 21 | calls: multicall.calls.map((batchCalls) => 22 | batchCalls.map((batchCall) => ({ 23 | ...batchCall, 24 | 25 | actions: batchCall.actions.map((action) => ({ 26 | ...action, 27 | 28 | args: JSON.parse(Base64.decode(action.args)), 29 | })), 30 | })) 31 | ), 32 | })), 33 | }, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/entities/job/job.model.ts: -------------------------------------------------------------------------------- 1 | import { JobData as JobDataOrig } from "../../shared/lib/contracts/multicall"; 2 | 3 | export type JobModel = { raw: JobData; normalized: JobData }; 4 | export type JobData = JobDataOrig; 5 | 6 | export const JobsSchema: { 7 | /** 8 | * Jobs indexed by ID for easy access to each particular job 9 | */ 10 | data: Record | null; 11 | error?: Error | null; 12 | loading: boolean; 13 | } = { 14 | data: null, 15 | error: null, 16 | loading: true, 17 | }; 18 | -------------------------------------------------------------------------------- /src/entities/job/job.service.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | 3 | import { Multicall } from "../../shared/lib/contracts/multicall"; 4 | 5 | import { JobLib } from "./job.lib"; 6 | import { JobsSchema } from "./job.model"; 7 | 8 | export interface IJobService { 9 | multicallInstance: Multicall; 10 | } 11 | 12 | export class JobService { 13 | private static readonly allEntriesFetch = async ( 14 | { multicallInstance }: IJobService, 15 | callback: (result: typeof JobsSchema) => void 16 | ) => 17 | void ( 18 | multicallInstance.ready && 19 | callback( 20 | await multicallInstance 21 | .getJobs() 22 | .then((data) => ({ 23 | data: data.reduce( 24 | (jobs, job) => ({ 25 | ...jobs, 26 | [job.id]: { 27 | raw: job, 28 | normalized: JobLib.toDecoded(job), 29 | }, 30 | }), 31 | {} 32 | ), 33 | error: null, 34 | loading: false, 35 | })) 36 | .catch((error) => ({ data: null, error, loading: false })) 37 | ) 38 | ); 39 | 40 | public static readonly useAllEntriesState = (inputs: IJobService) => { 41 | const [state, stateUpdate] = useState(JobsSchema); 42 | 43 | useEffect(() => { 44 | stateUpdate(JobsSchema); 45 | void JobService.allEntriesFetch(inputs, stateUpdate); 46 | }, [...Object.values(inputs), stateUpdate]); 47 | 48 | useEffect(() => { 49 | state.error instanceof Error && void console.error(state.error); 50 | }, [state.error]); 51 | 52 | return useMemo(() => state, [...Object.values(inputs), state]); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/entities/job/job.ui.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/font"; 3 | @use "sass/mixin"; 4 | @use "sass/size"; 5 | 6 | .Job { 7 | &-action { 8 | align-self: flex-end; 9 | border: 2px solid color.$blue; 10 | border-radius: size.$task-radius; 11 | padding: 0 1em; 12 | width: fit-content; 13 | background-color: rgba(color.$blue, 0.2); 14 | color: darken(color.$blue, 30%) !important; 15 | font-size: size.$text; 16 | font-weight: 800; 17 | 18 | &:hover { 19 | background-color: color.$blue; 20 | } 21 | } 22 | 23 | &-dataInspector { 24 | &-label { 25 | width: fit-content; 26 | margin-left: auto; 27 | } 28 | } 29 | 30 | &-multicalls { 31 | display: flex; 32 | flex-flow: column nowrap; 33 | gap: size.$gap * 0.5; 34 | 35 | &-inspector { 36 | display: flex; 37 | flex-flow: row wrap; 38 | justify-content: space-evenly; 39 | gap: size.$gap * 0.5; 40 | } 41 | 42 | &-item { 43 | display: flex; 44 | flex-flow: column nowrap; 45 | gap: size.$gap * 0.5; 46 | border: 2px solid color.$white; 47 | border-radius: size.$task-radius; 48 | padding: size.$gap * 0.5; 49 | min-width: 264px; 50 | height: fit-content; 51 | 52 | &-label { 53 | position: absolute; 54 | } 55 | } 56 | } 57 | } 58 | 59 | .JobEntriesTable { 60 | grid-area: JobEntriesTable; 61 | 62 | &-body { 63 | .TableRow-content--compact { 64 | &:not(&:first-of-type) { 65 | span:last-of-type { 66 | font-family: font.$code; 67 | } 68 | } 69 | 70 | &:last-of-type { 71 | align-items: flex-start; 72 | 73 | & > span { 74 | &:first-of-type { 75 | position: absolute; 76 | } 77 | 78 | &:last-of-type { 79 | width: 100%; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/context.ts: -------------------------------------------------------------------------------- 1 | import { MulticallSettingsParamKey } from "../../shared/lib/contracts/multicall"; 2 | import { SputnikDAO } from "../../shared/lib/contracts/sputnik-dao"; 3 | import { toYocto } from "../../shared/lib/converter"; 4 | 5 | /** 6 | * Type declaration for Multicall Instance entity 7 | */ 8 | export namespace MI { 9 | export interface Inputs { 10 | daoAddress: SputnikDAO["address"]; 11 | } 12 | 13 | export type ParamKey = MulticallSettingsParamKey; 14 | } 15 | 16 | /** 17 | * Multicall Instance entity config 18 | */ 19 | export class ModuleContext { 20 | public static readonly ParamKey = MulticallSettingsParamKey; 21 | 22 | /** 23 | * Minimum balance needed for storage + state. 24 | */ 25 | public static readonly MIN_BALANCE = toYocto(1); 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/index.ts: -------------------------------------------------------------------------------- 1 | import { MI as MIModule, ModuleContext } from "./context"; 2 | import { MIAdminsTable } from "./ui/mi-admins"; 3 | import { MITokenWhitelistTable } from "./ui/mi-token-whitelist"; 4 | 5 | export { type MIModule }; 6 | 7 | /** 8 | * Multicall Instance entity 9 | */ 10 | export class MI extends ModuleContext { 11 | static AdminsTable = MIAdminsTable; 12 | static TokenWhitelistTable = MITokenWhitelistTable; 13 | } 14 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/model/mi-admins.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { ArgsAccount } from "../../../shared/lib/args-old"; 4 | import { Multicall } from "../../../shared/lib/contracts/multicall"; 5 | import { Props } from "../../../shared/lib/props"; 6 | import { MI } from "../context"; 7 | 8 | import { MIInfoModel } from "./mi-settings"; 9 | 10 | export type MIAdminAddresses = { 11 | data: Multicall["admins"] | null; 12 | error: Error | null; 13 | loading: boolean; 14 | }; 15 | 16 | export class MIAdminsModel { 17 | static addressListFetchFx = async ( 18 | daoAddress: MI.Inputs["daoAddress"], 19 | callback: (result: MIAdminAddresses) => void 20 | ) => 21 | await MIInfoModel.dataFetchFx( 22 | `${ArgsAccount.deconstructAddress(daoAddress).name}.${Multicall.FACTORY_ADDRESS}`, 23 | (multicallInstanceData) => callback(Props.evolve({ data: ({ admins }) => admins }, multicallInstanceData)) 24 | ); 25 | 26 | static useAddressList = (daoAddress: MI.Inputs["daoAddress"]) => { 27 | const [state, stateUpdate] = useState({ 28 | data: null, 29 | error: null, 30 | loading: true, 31 | }); 32 | 33 | useEffect(() => void MIAdminsModel.addressListFetchFx(daoAddress, stateUpdate), [daoAddress, stateUpdate]); 34 | 35 | return state; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/model/mi-settings.ts: -------------------------------------------------------------------------------- 1 | import { ArgsAccount } from "../../../shared/lib/args-old"; 2 | import { Multicall } from "../../../shared/lib/contracts/multicall"; 3 | import { AccountId } from "../../../shared/lib/types"; 4 | 5 | export class MIInfoModel { 6 | /** 7 | * Calls the given callback with a result of multicall contract instantiation, 8 | * represented as stateful response. 9 | * 10 | * @param daoAddress DAO contract address 11 | * @param callback Stateful data fetch callback 12 | */ 13 | static dataFetchFx = async ( 14 | daoAddress: AccountId, 15 | callback: (result: { data: Multicall | null; error: Error | null; loading: boolean }) => void 16 | ) => 17 | callback( 18 | await Multicall.init( 19 | `${ArgsAccount.deconstructAddress(daoAddress).name}.${Multicall.FACTORY_ADDRESS}` 20 | ).then((multicallInstance) => ({ 21 | data: multicallInstance.ready ? multicallInstance : null, 22 | error: multicallInstance.ready ? null : new Error("Unable to connect to Multicall Instance"), 23 | loading: false, 24 | })) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/model/mi-tokens.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { ArgsAccount } from "../../../shared/lib/args-old"; 4 | import { Multicall } from "../../../shared/lib/contracts/multicall"; 5 | import { Props } from "../../../shared/lib/props"; 6 | import { MI } from "../context"; 7 | 8 | import { MIInfoModel } from "./mi-settings"; 9 | 10 | export type MITokenWhitelist = { 11 | data: Multicall["tokensWhitelist"] | null; 12 | error: Error | null; 13 | loading: boolean; 14 | }; 15 | 16 | export class MITokensModel { 17 | static whitelistFetchFx = async ( 18 | daoAddress: MI.Inputs["daoAddress"], 19 | callback: (result: MITokenWhitelist) => void 20 | ) => 21 | await MIInfoModel.dataFetchFx( 22 | `${ArgsAccount.deconstructAddress(daoAddress).name}.${Multicall.FACTORY_ADDRESS}`, 23 | 24 | (multicallInstanceData) => 25 | callback(Props.evolve({ data: ({ tokensWhitelist }) => tokensWhitelist }, multicallInstanceData)) 26 | ); 27 | 28 | static useWhitelist = (daoAddress: MI.Inputs["daoAddress"]) => { 29 | const [state, stateUpdate] = useState({ 30 | data: null, 31 | error: null, 32 | loading: true, 33 | }); 34 | 35 | useEffect(() => void MITokensModel.whitelistFetchFx(daoAddress, stateUpdate), [daoAddress, stateUpdate]); 36 | 37 | return state; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/ui/mi-admin.tsx: -------------------------------------------------------------------------------- 1 | import { NearLink, NearLinkProps } from "../../../shared/ui/design"; 2 | 3 | interface MIAdminProps extends NearLinkProps {} 4 | 5 | const MIAdmin = ({ address }: MIAdminProps) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export const miAdminAsTableRow = (item: MIAdminProps["address"]) => ({ 12 | content: [], 13 | id: item, 14 | }); 15 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/ui/mi-admins.tsx: -------------------------------------------------------------------------------- 1 | import { Scrollable, Table, Tile } from "../../../shared/ui/design"; 2 | import { MIAdminsModel } from "../model/mi-admins"; 3 | import { MI } from "../context"; 4 | 5 | import { miAdminAsTableRow } from "./mi-admin"; 6 | 7 | interface MIAdminsTableProps extends MI.Inputs { 8 | className?: string; 9 | } 10 | 11 | export const MIAdminsTable = ({ className, daoAddress }: MIAdminsTableProps) => { 12 | const { data, error, loading } = MIAdminsModel.useAddressList(daoAddress); 13 | 14 | return ( 15 | 21 | 22 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/ui/mi-token-whitelist.tsx: -------------------------------------------------------------------------------- 1 | import { Scrollable, Table, type TableProps, Tile, TileProps } from "../../../shared/ui/design"; 2 | import { MITokensModel } from "../model/mi-tokens"; 3 | import { MI } from "../context"; 4 | 5 | import { MIWhitelistedTokenProps, miWhitelistedTokenAsTableRow } from "./mi-whitelisted-token"; 6 | 7 | interface MITokenWhitelistTableProps extends MI.Inputs, Pick { 8 | ItemProps?: TableProps["RowProps"]; 9 | className?: string; 10 | itemsAdditional?: MIWhitelistedTokenProps["address"][]; 11 | onItemsSelected?: TableProps["onRowsSelected"]; 12 | } 13 | 14 | export const MITokenWhitelistTable = ({ 15 | ItemProps, 16 | className, 17 | daoAddress, 18 | footer, 19 | headerSlots, 20 | itemsAdditional, 21 | onItemsSelected, 22 | subheader, 23 | }: MITokenWhitelistTableProps) => { 24 | const { data, error, loading } = MITokensModel.useWhitelist(daoAddress); 25 | const allItems = data?.concat(itemsAdditional ?? []); 26 | 27 | return ( 28 | 34 | 35 |
43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/entities/multicall-instance/ui/mi-whitelisted-token.tsx: -------------------------------------------------------------------------------- 1 | import { FungibleToken } from "../../../shared/lib/standards/fungibleToken"; 2 | import { NearLink } from "../../../shared/ui/design"; 3 | 4 | export interface MIWhitelistedTokenProps { 5 | address: FungibleToken["address"]; 6 | } 7 | 8 | const MIWhitelistedToken = ({ address }: MIWhitelistedTokenProps) => ( 9 | 10 | 11 | 12 | ); 13 | 14 | export const miWhitelistedTokenAsTableRow = (item: MIWhitelistedTokenProps["address"]) => ({ 15 | content: [], 16 | id: item, 17 | }); 18 | -------------------------------------------------------------------------------- /src/entities/near-token/context.ts: -------------------------------------------------------------------------------- 1 | import { Multicall } from "../../shared/lib/contracts/multicall"; 2 | import { SputnikDAO } from "../../shared/lib/contracts/sputnik-dao"; 3 | 4 | export namespace NEARToken { 5 | export interface Inputs { 6 | adapters: { 7 | dao: SputnikDAO; 8 | multicall: Multicall; 9 | }; 10 | } 11 | } 12 | 13 | export class ModuleContext { 14 | static FRACTIONAL_PART_LENGTH = 5; 15 | } 16 | -------------------------------------------------------------------------------- /src/entities/near-token/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext, type NEARToken as NEARTokenModule } from "./context"; 2 | import { ntBalancesRender } from "./ui/nt-balances"; 3 | 4 | export { type NEARTokenModule }; 5 | 6 | export class NEARToken extends ModuleContext { 7 | static balancesRender = ntBalancesRender; 8 | } 9 | -------------------------------------------------------------------------------- /src/entities/near-token/model/near-balances.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { Big, formatTokenAmount } from "../../../shared/lib/converter"; 4 | import { viewAccount } from "../../../shared/lib/wallet"; 5 | import { ModuleContext, type NEARToken } from "../context"; 6 | 7 | type NEARTokenDataFxResponse = { 8 | data: { dao: string; multicall: string; total: string } | null; 9 | loading: boolean; 10 | }; 11 | 12 | const nearTokenDataFx = async ( 13 | { dao, multicall }: NEARToken.Inputs["adapters"], 14 | callback: (result: NEARTokenDataFxResponse) => void 15 | ) => { 16 | const [daoAccInfo, multicallAccInfo] = await Promise.all([ 17 | viewAccount(dao.address), 18 | viewAccount(multicall.address), 19 | ]); 20 | 21 | const daoRawBalance = daoAccInfo.amount, 22 | multicallRawBalance = multicallAccInfo.amount; 23 | 24 | return callback({ 25 | data: { 26 | dao: formatTokenAmount(daoRawBalance, 24, ModuleContext.FRACTIONAL_PART_LENGTH), 27 | multicall: formatTokenAmount(multicallRawBalance, 24, ModuleContext.FRACTIONAL_PART_LENGTH), 28 | 29 | total: formatTokenAmount( 30 | Big(daoRawBalance).add(multicallRawBalance).toFixed(), 31 | 24, 32 | ModuleContext.FRACTIONAL_PART_LENGTH 33 | ), 34 | }, 35 | 36 | loading: false, 37 | }); 38 | }; 39 | 40 | const useNEARTokenData = (adapters: NEARToken.Inputs["adapters"]) => { 41 | const [state, stateUpdate] = useState({ data: null, loading: true }); 42 | 43 | useEffect(() => void nearTokenDataFx(adapters, stateUpdate), [adapters, stateUpdate]); 44 | 45 | return state; 46 | }; 47 | 48 | export class NEARTokenBalancesModel { 49 | static useTokenFrom = useNEARTokenData; 50 | } 51 | -------------------------------------------------------------------------------- /src/entities/near-token/ui/nt-balances.tsx: -------------------------------------------------------------------------------- 1 | import { IconLabel, NearIcon } from "../../../shared/ui/design"; 2 | 3 | import { NEARTokenBalancesModel } from "../model/near-balances"; 4 | import { type NEARToken } from "../context"; 5 | 6 | interface NTBalancesRenderProps extends NEARToken.Inputs {} 7 | 8 | export const ntBalancesRender = ({ adapters }: NTBalancesRenderProps) => { 9 | const { data } = NEARTokenBalancesModel.useTokenFrom(adapters); 10 | 11 | return !data 12 | ? null 13 | : { 14 | content: [ 15 | } 17 | label="NEAR" 18 | />, 19 | 20 | data.multicall, 21 | data.dao, 22 | data.total, 23 | ], 24 | 25 | id: "NEAR", 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/entities/task/config/initial-data.ts: -------------------------------------------------------------------------------- 1 | export const initialData = { 2 | tasks: { 3 | "task-i1": { id: "task-i1", family: "", func: "" }, 4 | "task-i2": { id: "task-i2", family: "near", func: "ft_transfer" }, 5 | "task-i3": { id: "task-i3", family: "near", func: "ft_transfer_call" }, 6 | "task-i4": { id: "task-i4", family: "near", func: "nft_transfer" }, 7 | "task-i5": { id: "task-i5", family: "near", func: "nft_transfer_call" }, 8 | "task-i6": { id: "task-i6", family: "near", func: "nft_approve" }, 9 | "task-i7": { id: "task-i7", family: "near", func: "nft_revoke" }, 10 | "task-i8": { id: "task-i8", family: "near", func: "mft_transfer" }, 11 | "task-i9": { id: "task-i9", family: "near", func: "mft_transfer_call" }, 12 | "task-i10": { id: "task-i10", family: "near", func: "deposit_and_stake" }, 13 | "task-i11": { id: "task-i11", family: "near", func: "unstake" }, 14 | "task-i12": { id: "task-i12", family: "near", func: "withdraw" }, 15 | "task-i13": { id: "task-i13", family: "near", func: "storage_withdraw" }, 16 | "task-i14": { id: "task-i14", family: "near", func: "storage_unregister" }, 17 | "task-i15": { id: "task-i15", family: "near", func: "storage_deposit" }, 18 | "task-i16": { id: "task-i16", family: "near", func: "near_deposit" }, 19 | "task-i17": { id: "task-i17", family: "near", func: "near_withdraw" }, 20 | "task-i18": { id: "task-i18", family: "multicall", func: "near_transfer" }, 21 | "task-i19": { id: "task-i19", family: "mintbase", func: "create_store" }, 22 | "task-i20": { id: "task-i20", family: "mintbase", func: "transfer_store_ownership" }, 23 | "task-i21": { id: "task-i21", family: "mintbase", func: "grant_minter" }, 24 | "task-i22": { id: "task-i22", family: "mintbase", func: "revoke_minter" }, 25 | "task-i23": { id: "task-i23", family: "mintbase", func: "buy" }, 26 | "task-i24": { id: "task-i24", family: "paras", func: "buy" }, 27 | "task-i25": { id: "task-i25", family: "token_farm", func: "create_token" }, 28 | "task-i26": { id: "task-i26", family: "meta-pool", func: "nslp_add_liquidity" }, 29 | "task-i27": { id: "task-i27", family: "meta-pool", func: "nslp_remove_liquidity" }, 30 | }, 31 | columns: { 32 | "column-0": { 33 | id: "column-0", 34 | title: "Drag here", 35 | taskIds: [], 36 | }, 37 | menu: { 38 | id: "menu", 39 | title: "Infinite Column", 40 | taskIds: [ 41 | "task-i1", 42 | "task-i2", 43 | "task-i3", 44 | "task-i4", 45 | "task-i5", 46 | "task-i6", 47 | "task-i7", 48 | "task-i8", 49 | "task-i9", 50 | "task-i10", 51 | "task-i11", 52 | "task-i12", 53 | "task-i13", 54 | "task-i14", 55 | "task-i15", 56 | "task-i16", 57 | "task-i17", 58 | "task-i18", 59 | "task-i19", 60 | "task-i20", 61 | "task-i21", 62 | "task-i22", 63 | "task-i23", 64 | //"task-i24", 65 | "task-i25", 66 | "task-i26", 67 | "task-i27", 68 | ], 69 | }, 70 | }, 71 | columnOrder: ["column-0"], 72 | }; 73 | -------------------------------------------------------------------------------- /src/entities/task/config/keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords = { 2 | "": { 3 | "": ["custom"], 4 | }, 5 | near: { 6 | ft_transfer: ["pay", "fungible", "token"], 7 | ft_transfer_call: ["pay", "fungible", "token"], 8 | nft_transfer: ["token", "non fungible"], 9 | nft_transfer_call: ["token", "non fungible"], 10 | nft_approve: ["token", "non fungible"], 11 | nft_revoke: ["revoke_all", "token", "non fungible"], 12 | mft_transfer: ["LP", "token"], 13 | mft_transfer_call: ["LP", "token"], 14 | deposit_and_stake: ["validator"], 15 | unstake: ["validator", "unstake_all"], 16 | withdraw: ["validator"], 17 | storage_withdraw: [], 18 | storage_unregister: [], 19 | storage_deposit: [], 20 | near_deposit: ["wnear", "wrapped", "wrap", "fungible", "token"], 21 | near_withdraw: ["wnear", "wrapped", "unwrap", "fungible", "token"], 22 | }, 23 | multicall: { 24 | near_transfer: ["pay"], 25 | }, 26 | mintbase: { 27 | create_store: ["create store"], 28 | grant_minter: ["add minter"], 29 | revoke_minter: ["remove minter"], 30 | transfer_store_ownership: ["transfer store ownership"], 31 | buy: ["nft", "marketplace"], 32 | }, 33 | paras: { 34 | buy: ["nft", "marketplace"], 35 | }, 36 | token_farm: { 37 | create_token: ["TokenFarm", "treasury"], 38 | }, 39 | "meta-pool": { 40 | nslp_add_liquidity: ["add liquidity", "lp"], 41 | nslp_remove_liquidity: ["remove liquidity", "lp"], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/entities/task/index.ts: -------------------------------------------------------------------------------- 1 | export { Task } from "./ui/task"; 2 | -------------------------------------------------------------------------------- /src/entities/task/ui/task.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/color"; 3 | 4 | .task-wrapper { 5 | width: size.$task-width; 6 | min-height: size.$task-height; 7 | border-radius: size.$task-radius; 8 | margin-bottom: size.$gap; 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/entities/wallet/index.ts: -------------------------------------------------------------------------------- 1 | import { WalletComponent } from "./ui/wallet"; 2 | import { tryWalletSelectorContext, WalletSelectorContext, WalletSelectorContextProvider } from "./ui/providers"; 3 | 4 | export class Wallet { 5 | static Selector = WalletComponent; 6 | static SelectorContext = WalletSelectorContext; 7 | static SelectorContextProvider = WalletSelectorContextProvider; 8 | static trySelectorContext = tryWalletSelectorContext; 9 | } 10 | -------------------------------------------------------------------------------- /src/families/custom.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "./base"; 3 | 4 | .custom-task { 5 | background: color.$lightest; 6 | @include base.task-theme(color.$text); 7 | * > a { 8 | color: darken(color.$blue, 20%) !important; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/families/families.ts: -------------------------------------------------------------------------------- 1 | import { CustomTask } from "./custom"; 2 | 3 | import * as Multicall from "./multicall/family"; 4 | import * as TknFarm from "./token-farm/family"; 5 | import * as Near from "./near/family"; 6 | import * as Mintbase from "./mintbase/family"; 7 | import * as Paras from "./paras/family"; 8 | import * as MetaPool from "./meta-pool/family"; 9 | 10 | export { CustomTask, Multicall, Near, Mintbase, Paras, TknFarm, MetaPool }; 11 | -------------------------------------------------------------------------------- /src/families/index.ts: -------------------------------------------------------------------------------- 1 | export * as Family from "./families"; 2 | -------------------------------------------------------------------------------- /src/families/meta-pool/family.ts: -------------------------------------------------------------------------------- 1 | export { NslpAddLiquidity } from "./nslp-add-liquidity"; 2 | export { NslpRemoveLiquidity } from "./nslp-remove-liquidity"; 3 | -------------------------------------------------------------------------------- /src/families/meta-pool/meta-pool.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/mixin"; 3 | @use "sass/font"; 4 | @use "../base"; 5 | 6 | $meta-pool-purple: #735de9; 7 | 8 | .meta-pool-nslp-add-liquidity-task, 9 | .meta-pool-nslp-remove-liquidity-task { 10 | background-color: $meta-pool-purple; 11 | background-image: url("../../app/static/meta-pool/MetaPool_symbol.svg"); 12 | background-position-x: 0rem; 13 | background-position-y: -2rem; 14 | background-repeat: no-repeat; 15 | background-size: 12rem; 16 | @include base.task-theme(color.$light-text); 17 | } 18 | 19 | .meta-pool-nslp-add-liquidity-task-edit, 20 | .meta-pool-nslp-remove-liquidity-task-edit { 21 | .InfoField-content { 22 | display: flex; 23 | flex-flow: column nowrap; 24 | .entry { 25 | @include mixin.center-items(space-between, center); 26 | height: 2em; 27 | flex-flow: row nowrap; 28 | max-width: 100%; 29 | } 30 | .key { 31 | font-weight: 800; 32 | white-space: nowrap; 33 | } 34 | .value { 35 | font-family: font.$code; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | margin-left: 2em; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/families/mintbase/family.ts: -------------------------------------------------------------------------------- 1 | export { CreateStore } from "./create-store"; 2 | export { TransferStoreOwnership } from "./transfer-store-ownership"; 3 | export { AddMinter } from "./add-minter"; 4 | export { RemoveMinter } from "./remove-minter"; 5 | export { BuyNft } from "./buy-nft"; 6 | -------------------------------------------------------------------------------- /src/families/mintbase/mintbase.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/mixin"; 3 | @use "sass/font"; 4 | @use "../base"; 5 | 6 | $mintbase-blue: #070c2b; 7 | $mintbase-red: #ff2424; 8 | 9 | .mintbase-create-store-task, 10 | .mintbase-transfer-store-ownership-task, 11 | .mintbase-add-minter-task, 12 | .mintbase-remove-minter-task, 13 | .mintbase-buy-nft-task { 14 | background-color: $mintbase-blue; 15 | background-image: url("../../app/static/mintbase/Mintbase_symbol.svg"); 16 | background-position-x: 42rem; 17 | background-position-y: 239rem; 18 | background-size: 77rem; 19 | @include base.task-theme(color.$light-text); 20 | } 21 | 22 | .mintbase-add-minter-task-edit, 23 | .mintbase-remove-minter-task-edit { 24 | .InfoField-content { 25 | display: flex; 26 | flex-flow: column nowrap; 27 | .minter-account-id { 28 | align-self: flex-end; 29 | } 30 | } 31 | } 32 | 33 | .mintbase-buy-nft-task-edit { 34 | .InfoField-content { 35 | display: flex; 36 | flex-flow: column nowrap; 37 | .entry { 38 | @include mixin.center-items(space-between, center); 39 | height: 2em; 40 | flex-flow: row nowrap; 41 | max-width: 100%; 42 | } 43 | .key { 44 | font-weight: 800; 45 | white-space: nowrap; 46 | } 47 | .value { 48 | font-family: font.$code; 49 | white-space: nowrap; 50 | overflow: hidden; 51 | text-overflow: ellipsis; 52 | margin-left: 2em; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/families/multicall/family.ts: -------------------------------------------------------------------------------- 1 | export { Transfer } from "./transfer"; 2 | -------------------------------------------------------------------------------- /src/families/multicall/multicall.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "../base"; 3 | 4 | .multicall-transfer-task { 5 | background: color.$darkest; 6 | @include base.task-theme(color.$light-text); 7 | } 8 | -------------------------------------------------------------------------------- /src/families/near/family.ts: -------------------------------------------------------------------------------- 1 | export { DepositAndStake } from "./deposit-and-stake"; 2 | export { FtTransfer } from "./ft-transfer"; 3 | export { FtTransferCall } from "./ft-transfer-call"; 4 | export { MftTransfer } from "./mft-transfer"; 5 | export { MftTransferCall } from "./mft-transfer-call"; 6 | export { NftTransfer } from "./nft-transfer"; 7 | export { NftTransferCall } from "./nft-transfer-call"; 8 | export { NftApprove } from "./nft-approve"; 9 | export { NftRevoke } from "./nft-revoke"; 10 | export { StorageDeposit } from "./storage-deposit"; 11 | export { StorageUnregister } from "./storage-unregister"; 12 | export { StorageWithdraw } from "./storage-withdraw"; 13 | export { Unstake } from "./unstake"; 14 | export { Withdraw } from "./withdraw"; 15 | export { WrapNear } from "./wrap-near"; 16 | export { UnwrapNear } from "./unwrap-near"; 17 | -------------------------------------------------------------------------------- /src/families/near/near.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/mixin"; 3 | @use "sass/size"; 4 | @use "sass/font"; 5 | @use "../base"; 6 | 7 | .near-storage-withdraw-task, 8 | .near-storage-deposit-task, 9 | .near-storage-unregister-task, 10 | .near-ft-transfer-task, 11 | .near-ft-transfer-call-task, 12 | .near-nft-transfer-task, 13 | .near-nft-transfer-call-task, 14 | .near-nft-approve-task, 15 | .near-nft-revoke-task, 16 | .near-mft-transfer-task, 17 | .near-mft-transfer-call-task, 18 | .near-deposit-and-stake-task, 19 | .near-unstake-task, 20 | .near-withdraw-task, 21 | .near-wrap-task, 22 | .near-unwrap-task { 23 | background: color.$lightest; 24 | @include base.task-theme(color.$text); 25 | * > a { 26 | color: darken(color.$blue, 20%) !important; 27 | } 28 | } 29 | 30 | .near-wrap-task-edit, 31 | .near-unwrap-task-edit { 32 | .InfoField-content { 33 | display: flex; 34 | flex-flow: column nowrap; 35 | .entry { 36 | @include mixin.center-items(space-between, center); 37 | height: 2em; 38 | flex-flow: row nowrap; 39 | max-width: 100%; 40 | } 41 | .key { 42 | font-weight: 800; 43 | white-space: nowrap; 44 | } 45 | .value { 46 | font-family: font.$code; 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | margin-left: 2em; 51 | } 52 | } 53 | } 54 | 55 | .near-nft-approve-task-edit, 56 | .near-nft-revoke-task-edit { 57 | .InfoField-content { 58 | display: flex; 59 | flex-flow: column nowrap; 60 | .approved-account-id { 61 | align-self: flex-end; 62 | } 63 | } 64 | } 65 | 66 | .MuiInputAdornment-root { 67 | width: fit-content; 68 | p { 69 | display: block !important; 70 | font-size: size.$small-text !important; 71 | color: color.$lightish; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/families/paras/family.ts: -------------------------------------------------------------------------------- 1 | export { BuyNft } from "./buy-nft"; 2 | -------------------------------------------------------------------------------- /src/families/paras/paras.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/mixin"; 3 | @use "sass/font"; 4 | @use "../base"; 5 | 6 | $paras-blue: #1300be; 7 | $paras-black: #04002a; 8 | 9 | .paras-buy-nft-task { 10 | background: radial-gradient(at bottom left, $paras-blue 0%, $paras-black 100%); 11 | @include base.task-theme(color.$light-text); 12 | } 13 | 14 | .paras-buy-nft-task-edit { 15 | .InfoField-content { 16 | display: flex; 17 | flex-flow: column nowrap; 18 | .entry { 19 | @include mixin.center-items(space-between, center); 20 | height: 2em; 21 | flex-flow: row nowrap; 22 | max-width: 100%; 23 | } 24 | .key { 25 | font-weight: 800; 26 | white-space: nowrap; 27 | } 28 | .value { 29 | font-family: font.$code; 30 | white-space: nowrap; 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | margin-left: 2em; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/families/token-farm/family.ts: -------------------------------------------------------------------------------- 1 | export { CreateToken } from "./create-token"; 2 | -------------------------------------------------------------------------------- /src/families/token-farm/token-farm.scss: -------------------------------------------------------------------------------- 1 | @use "../base"; 2 | @use "sass/color"; 3 | 4 | .token-farm-create-token-task { 5 | background-color: color.$yellow; 6 | background-image: url("../../app/static/token-farm/TokenFarm_symbol.png"); 7 | background-size: 60rem; 8 | // background-blend-mode: luminosity; 9 | @include base.task-theme(color.$text); 10 | * > a { 11 | color: darken(color.$blue, 30%) !important; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/features/external-login/context.ts: -------------------------------------------------------------------------------- 1 | export class ModuleContext { 2 | static KEYS = { 3 | all: "ed25519%3A9jeqkc8ybv7aYSA7uLNFUEn8cgKo759yue4771bBWsSr", 4 | public: "ed25519%3ADEaoD65LomNHAMzhNZva15LC85ntwBHdcTbCnZRXciZH", 5 | }; 6 | 7 | static METHODS: Record<"dao" | "multicall", { title: string; type: keyof typeof ModuleContext.METHODS }> = { 8 | dao: { title: "Login in dApps as DAO", type: "dao" }, 9 | multicall: { title: "Login in dApps as Multicall", type: "multicall" }, 10 | }; 11 | 12 | static STEP_BY_STEP_GUIDE = [ 13 | { 14 | text: "Open the dApp in another browser tab", 15 | }, 16 | { 17 | text: "Log out your account on the dApp", 18 | hint: "You should not be logged in with any wallet on the other dApp, otherwise this won't work.", 19 | }, 20 | { 21 | text: "Copy the dApp's URL", 22 | }, 23 | { 24 | text: "Paste the URL in the input field below", 25 | }, 26 | { 27 | text: 'Click "Proceed"', 28 | hint: 'This opens the dApp in a new tab, with a "watch-only" mode. Meaning you cannot sign transactions with it', 29 | }, 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/features/external-login/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext } from "./context"; 2 | import { ELDialogs } from "./ui/el-dialogs"; 3 | import { ELMenu } from "./ui/el-menu"; 4 | 5 | export class ExternalLogin extends ModuleContext { 6 | static Dialogs = ELDialogs; 7 | static Menu = ELMenu; 8 | } 9 | -------------------------------------------------------------------------------- /src/features/external-login/model/el-dialogs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * !TODO: Use 3rd party solution for dialogs management 3 | */ 4 | 5 | import { useCallback, useEffect, useState } from "react"; 6 | 7 | import { ModuleContext } from "../context"; 8 | 9 | const _dialogOpenRequested = "dialogOpenRequested"; 10 | 11 | const dialogOpenRequested = { 12 | dispatch: (dialogKey: keyof typeof ModuleContext.METHODS) => 13 | document.dispatchEvent(new CustomEvent(_dialogOpenRequested, { detail: { dialogKey } })), 14 | 15 | subscribe: (callback: EventListener) => { 16 | document.addEventListener(_dialogOpenRequested, callback); 17 | 18 | return () => document.removeEventListener(_dialogOpenRequested, callback); 19 | }, 20 | }; 21 | 22 | export class ELDialogsModel { 23 | static dialogOpenRequested = dialogOpenRequested.dispatch; 24 | 25 | static useVisibilityState = () => { 26 | const [dialogsVisibility, dialogVisibilitySwitch] = useState< 27 | Record 28 | >({ 29 | dao: false, 30 | multicall: false, 31 | }); 32 | 33 | useEffect( 34 | () => 35 | dialogOpenRequested.subscribe((event) => 36 | dialogVisibilitySwitch( 37 | Object.keys(dialogsVisibility).reduce( 38 | (visibilityState, someDialogKey) => ({ 39 | ...visibilityState, 40 | [someDialogKey]: someDialogKey === (event).detail.dialogKey ? true : false, 41 | }), 42 | 43 | dialogsVisibility 44 | ) 45 | ) 46 | ), 47 | [] 48 | ); 49 | 50 | return { 51 | dialogsVisibility, 52 | 53 | closeHandlerBinding: useCallback( 54 | (dialogKey: string) => () => dialogVisibilitySwitch({ ...dialogsVisibility, [dialogKey]: false }), 55 | [dialogVisibilitySwitch] 56 | ), 57 | }; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/features/external-login/ui/el-dialog.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/mixin"; 3 | 4 | .ExternalLoginDialog { 5 | &-stepByStepGuide { 6 | padding: 0 size.$gap; 7 | list-style-type: decimal; 8 | font-size: size.$text; 9 | 10 | li { 11 | span { 12 | @include mixin.center-items(flex-start, center); 13 | flex-flow: row nowrap; 14 | gap: 0.5ch; 15 | padding-left: 0.25 * size.$gap; 16 | 17 | .MuiSvgIcon-root { 18 | @include mixin.light-icon; 19 | } 20 | } 21 | 22 | &::marker { 23 | font-weight: 800; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/features/external-login/ui/el-menu.tsx: -------------------------------------------------------------------------------- 1 | import { PreviewOutlined } from "@mui/icons-material"; 2 | import { ComponentProps } from "react"; 3 | 4 | import { PopupMenu } from "../../../shared/ui/design"; 5 | import { ModuleContext as ModuleContext } from "../context"; 6 | import { ELDialogsModel } from "../model/el-dialogs"; 7 | 8 | interface ELMenuProps extends Pick, "triggerClassName"> { 9 | FeatureFlags: { 10 | ExternalLogin: Record; 11 | }; 12 | } 13 | 14 | export const ELMenu = ({ FeatureFlags, triggerClassName }: ELMenuProps) => ( 15 | } 17 | items={Object.values(ModuleContext.METHODS).map(({ title, type }) => ({ 18 | disabled: !FeatureFlags.ExternalLogin[type], 19 | key: type, 20 | onClick: () => ELDialogsModel.dialogOpenRequested(type), 21 | title, 22 | }))} 23 | {...{ triggerClassName }} 24 | /> 25 | ); 26 | -------------------------------------------------------------------------------- /src/features/index.ts: -------------------------------------------------------------------------------- 1 | export { ExternalLogin } from "./external-login"; 2 | export { SchedulingSettingsChange, type SchedulingSettingsChangeModule } from "./scheduling/settings-change"; 3 | export { TokenWhitelistChange, type TokenWhitelistChangeModule } from "./tokens/whitelist-change"; 4 | -------------------------------------------------------------------------------- /src/features/scheduling/settings-change/context.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from "react"; 2 | 3 | import { MI, MIModule } from "../../../entities"; 4 | import { MulticallSettingsDiff, Multicall } from "../../../shared/lib/contracts/multicall"; 5 | import { DesignContext } from "../../../shared/ui/design"; 6 | 7 | export namespace SchedulingSettingsChange { 8 | export type DiffKey = MIModule.ParamKey; 9 | 10 | export type FormState = Pick; 11 | 12 | export interface Inputs extends Omit, "onChange">, Pick { 13 | multicallInstance: Multicall; 14 | onEdit: (payload: FormState) => void; 15 | resetTrigger: { subscribe: (callback: EventListener) => () => void }; 16 | } 17 | } 18 | 19 | export class ModuleContext { 20 | public static readonly DiffKey = MI.ParamKey; 21 | 22 | public static readonly DiffMeta = { 23 | [ModuleContext.DiffKey.croncatManager]: { 24 | color: "blue" as DesignContext.Color, 25 | description: "Croncat manager", 26 | }, 27 | 28 | [ModuleContext.DiffKey.jobBond]: { 29 | color: "blue" as DesignContext.Color, 30 | description: "Job bond", 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/features/scheduling/settings-change/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext, type SchedulingSettingsChange as SchedulingSettingsChangeModule } from "./context"; 2 | import { SchedulingSettingsForm } from "./ui/scheduling-settings-form"; 3 | 4 | export class SchedulingSettingsChange extends ModuleContext { 5 | static Form = SchedulingSettingsForm; 6 | } 7 | 8 | export { type SchedulingSettingsChangeModule }; 9 | -------------------------------------------------------------------------------- /src/features/tokens/whitelist-change/context.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from "react"; 2 | 3 | import { MulticallTokenWhitelistDiffKey, type MulticallSettingsDiff } from "../../../shared/lib/contracts/multicall"; 4 | import { MIModule } from "../../../entities"; 5 | import { DesignContext } from "../../../shared/ui/design"; 6 | 7 | export namespace TokenWhitelistChange { 8 | export type DiffKey = MulticallTokenWhitelistDiffKey; 9 | 10 | export interface Inputs extends Omit, "onChange">, MIModule.Inputs { 11 | onEdit: (payload: Pick) => void; 12 | resetTrigger: { subscribe: (callback: EventListener) => () => void }; 13 | } 14 | 15 | export interface FormStates 16 | extends Record< 17 | keyof Pick, 18 | Set][number]> 19 | > {} 20 | } 21 | 22 | export class ModuleContext { 23 | public static readonly DiffKey = MulticallTokenWhitelistDiffKey; 24 | 25 | public static readonly DiffMeta = { 26 | [ModuleContext.DiffKey.addTokens]: { 27 | color: "green" as DesignContext.Color, 28 | description: "Tokens to add to whitelist", 29 | }, 30 | 31 | [ModuleContext.DiffKey.removeTokens]: { 32 | color: "red" as DesignContext.Color, 33 | description: "Tokens to remove from whitelist", 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/features/tokens/whitelist-change/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext, type TokenWhitelistChange as TokenWhitelistChangeModule } from "./context"; 2 | import { TokenWhitelistForm } from "./ui/tokens-whitelist-form"; 3 | 4 | export class TokenWhitelistChange extends ModuleContext { 5 | static Form = TokenWhitelistForm; 6 | } 7 | 8 | export { type TokenWhitelistChangeModule }; 9 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/font"; 4 | @use "sass/mixin"; 5 | 6 | * { 7 | margin: 0; 8 | font-family: font.$text; 9 | } 10 | 11 | body, 12 | html, 13 | #root { 14 | width: 100%; 15 | height: 100vh; 16 | background-color: color.$white; 17 | font-size: clamp(1.8mm, 0.6vw, 2.5mm); 18 | } 19 | 20 | body { 21 | // wallet selector style override 22 | --wallet-selector-content-bg: #{color.$black}; 23 | } 24 | 25 | a { 26 | color: darken(color.$blue, 30%); 27 | text-decoration: none; 28 | cursor: pointer; 29 | } 30 | 31 | button { 32 | padding: 0.5 * size.$gap 2 * size.$gap; 33 | background-color: color.$light; 34 | border: none; 35 | color: color.$text; 36 | font-family: font.$text; 37 | font-size: size.$large-text; 38 | cursor: pointer; 39 | } 40 | 41 | .MuiCheckbox-root { 42 | .MuiSvgIcon-root { 43 | width: size.$large-text; 44 | height: size.$large-text; 45 | } 46 | } 47 | 48 | .MuiTextField-root { 49 | .MuiInputLabel-root, 50 | .MuiInputBase-root { 51 | font-size: size.$text; 52 | 53 | input { 54 | padding: calc(size.$gap * 0.4) calc(size.$gap * 0.6); 55 | font-size: size.$text; 56 | } 57 | 58 | fieldset { 59 | border-radius: size.$task-radius; 60 | } 61 | } 62 | } 63 | 64 | .disabled { 65 | background-color: color.$darkest !important; 66 | color: rgba(color.$light-text, 0.3); 67 | cursor: not-allowed !important; 68 | } 69 | 70 | .hidden { 71 | display: none; 72 | } 73 | 74 | .warn { 75 | color: color.$yellow; 76 | } 77 | 78 | .font--code { 79 | font-family: font.$code; 80 | } 81 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | near-multicall 6 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "@near-wallet-selector/modal-ui/styles.css"; 2 | import { lazy, Suspense } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { HashRouter, Routes, Route, Navigate } from "react-router-dom"; 5 | 6 | const AppPage = lazy(() => import("./pages/app")); 7 | const DaoPage = lazy(() => import("./pages/dao")); 8 | import { Wallet } from "./entities"; 9 | import { DialogsLayer, Sidebar } from "./widgets"; 10 | import "./shared/lib/persistent"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 23 | } 24 | /> 25 | 26 | 30 | 31 | Loading...}> 32 | 33 | 34 | 35 | 36 | } 37 | /> 38 | 39 | 43 | 44 | Loading...}> 45 | 46 | 47 | 48 | 49 | } 50 | /> 51 | 52 | 53 | , 54 | 55 | document.querySelector("#root") 56 | ); 57 | -------------------------------------------------------------------------------- /src/near-config.ts: -------------------------------------------------------------------------------- 1 | function getConfig(env: string) { 2 | switch (env) { 3 | case "production": 4 | case "mainnet": 5 | return { 6 | networkId: "mainnet", 7 | WNEAR_ADDRESS: "wrap.near", 8 | EXAMPLE_ADDRESS: "example.near", 9 | REF_EXCHANGE_ADDRESS: "v2.ref-finance.near", 10 | CRONCAT_MANAGER_ADDRESS: "manager_v1.croncat.near", 11 | nodeUrl: "https://rpc.mainnet.near.org", 12 | walletUrl: "https://wallet.near.org", 13 | helperUrl: "https://api.kitwallet.app", 14 | explorerUrl: "https://explorer.mainnet.near.org", 15 | }; 16 | case "development": 17 | case "testnet": 18 | return { 19 | networkId: "testnet", 20 | WNEAR_ADDRESS: "wrap.testnet", 21 | EXAMPLE_ADDRESS: "example.testnet", 22 | REF_EXCHANGE_ADDRESS: "ref-finance-101.testnet", 23 | CRONCAT_MANAGER_ADDRESS: "manager_v1.croncat.testnet", 24 | nodeUrl: "https://rpc.testnet.near.org", 25 | walletUrl: "https://wallet.testnet.near.org", 26 | helperUrl: "https://testnet-api.kitwallet.app", 27 | explorerUrl: "https://explorer.testnet.near.org", 28 | }; 29 | case "betanet": 30 | return { 31 | networkId: "betanet", 32 | nodeUrl: "https://rpc.betanet.near.org", 33 | walletUrl: "https://wallet.betanet.near.org", 34 | helperUrl: "https://helper.betanet.near.org", 35 | explorerUrl: "https://explorer.betanet.near.org", 36 | }; 37 | case "local": 38 | return { 39 | networkId: "local", 40 | nodeUrl: "http://localhost:3030", 41 | keyPath: `${process.env.HOME}/.near/validator_key.json`, 42 | walletUrl: "http://localhost:4000/wallet", 43 | }; 44 | case "test": 45 | case "ci": 46 | return { 47 | networkId: "shared-test", 48 | nodeUrl: "https://rpc.ci-testnet.near.org", 49 | masterAccount: "test.near", 50 | }; 51 | case "ci-betanet": 52 | return { 53 | networkId: "shared-test-staging", 54 | nodeUrl: "https://rpc.ci-betanet.near.org", 55 | masterAccount: "test.near", 56 | }; 57 | default: 58 | throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); 59 | } 60 | } 61 | 62 | export { getConfig }; 63 | -------------------------------------------------------------------------------- /src/pages/app/app.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | @use "sass/color"; 4 | @use "sass/animation"; 5 | 6 | @mixin tutorial { 7 | content: " "; 8 | position: absolute; 9 | height: 40vh; 10 | top: 30vh; 11 | background-size: contain; 12 | background-repeat: no-repeat; 13 | opacity: 0.2; 14 | filter: none; 15 | transition: all 1s ease; 16 | } 17 | 18 | .layout-wrapper { 19 | @include mixin.page-wrapper; 20 | display: flex; 21 | flex-flow: row nowrap; 22 | .layout-container { 23 | @include mixin.full; 24 | @include mixin.center-items(center, flex-start); 25 | flex: 1 1 0; 26 | flex-flow: row nowrap; 27 | padding-left: size.$gap; 28 | width: auto; 29 | &[tutorial="yes"]::before { 30 | @include tutorial; 31 | width: 28%; 32 | left: 15%; 33 | background-image: url("../../app/static/sequence.svg"); 34 | background-position: left; 35 | } 36 | &[tutorial="no"]::before { 37 | @include tutorial; 38 | width: 28%; 39 | left: 15%; 40 | background-image: url("../../app/static/sequence.svg"); 41 | background-position: left; 42 | opacity: 0; 43 | filter: blur(5px); 44 | } 45 | &[tutorial="yes"]::after { 46 | @include tutorial; 47 | width: 35%; 48 | right: 15%; 49 | background-image: url("../../app/static/parallel.svg"); 50 | background-position: right; 51 | } 52 | &[tutorial="no"]::after { 53 | @include tutorial; 54 | width: 35%; 55 | right: 15%; 56 | background-image: url("../../app/static/parallel.svg"); 57 | background-position: right; 58 | opacity: 0; 59 | filter: blur(5px); 60 | } 61 | } 62 | .empty-container { 63 | flex-shrink: 0; 64 | flex-basis: calc(4 * size.$gap + size.$task-width); 65 | transition: flex-basis animation.$menu-expand-time ease-out; 66 | &.expanded-empty { 67 | flex-basis: calc(2 * size.$gap + animation.$menu-expand-width); 68 | transition: flex-basis animation.$menu-expand-time ease-out; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/pages/app/index.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from "./app"; 2 | export default AppPage; 3 | -------------------------------------------------------------------------------- /src/pages/dao/funds/funds.scss: -------------------------------------------------------------------------------- 1 | .DaoFundsTab { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | grid-template-rows: 1fr 1fr; 5 | 6 | grid-template-areas: 7 | "TokenBalances TokenBalances" 8 | "TokenBalances TokenBalances"; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/dao/funds/funds.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLProps } from "react"; 3 | 4 | import { TokenBalances, type TokenBalancesModule } from "../../../widgets"; 5 | 6 | import "./funds.scss"; 7 | 8 | interface DaoFundsTabUIProps extends HTMLProps, TokenBalancesModule.Inputs {} 9 | 10 | const _DaoFundsTab = "DaoFundsTab"; 11 | 12 | const DaoFundsTabUI = ({ className, adapters, ...props }: DaoFundsTabUIProps) => ( 13 |
17 | 18 |
19 | ); 20 | 21 | export const DaoFundsTab = { 22 | uiConnect: (props: DaoFundsTabUIProps) => ({ 23 | content: , 24 | lazy: true, 25 | name: "Funds", 26 | }), 27 | }; 28 | -------------------------------------------------------------------------------- /src/pages/dao/index.ts: -------------------------------------------------------------------------------- 1 | import { DaoPage } from "./dao"; 2 | export default DaoPage; 3 | -------------------------------------------------------------------------------- /src/pages/dao/jobs/jobs.scss: -------------------------------------------------------------------------------- 1 | .DaoJobsTab { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | grid-template-rows: 1fr 1fr; 5 | 6 | grid-template-areas: 7 | "JobEntriesTable JobEntriesTable" 8 | "JobEntriesTable JobEntriesTable"; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/dao/jobs/jobs.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ComponentProps, HTMLProps } from "react"; 3 | 4 | import { Job } from "../../../entities"; 5 | 6 | import "./jobs.scss"; 7 | 8 | interface DaoJobsTabUIProps extends HTMLProps, ComponentProps {} 9 | 10 | const _DaoJobsTab = "DaoJobsTab"; 11 | 12 | const DaoJobsTabUI = ({ className, multicallInstance, ...props }: DaoJobsTabUIProps) => ( 13 |
17 | 18 |
19 | ); 20 | 21 | export const DaoJobsTab = { 22 | uiConnect: (props: DaoJobsTabUIProps) => ({ 23 | content: , 24 | lazy: true, 25 | name: "Jobs", 26 | }), 27 | }; 28 | -------------------------------------------------------------------------------- /src/pages/dao/settings/settings.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | 4 | .DaoSettingsTab { 5 | margin: size.$gap !important; 6 | padding: 0 !important; 7 | overflow-y: scroll; 8 | border-radius: size.$task-radius; 9 | @include mixin.no-scrollbar; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/dao/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLProps } from "react"; 3 | 4 | import { SettingsEditor, SettingsEditorModule } from "../../../widgets"; 5 | 6 | import "./settings.scss"; 7 | 8 | interface DaoSettingsTabUIProps extends HTMLProps, SettingsEditorModule.Inputs {} 9 | 10 | const _DaoSettingsTab = "DaoSettingsTab"; 11 | 12 | const DaoSettingsTabUI = ({ className, adapters, ...props }: DaoSettingsTabUIProps) => ( 13 |
17 | 18 |
19 | ); 20 | 21 | export const DaoSettingsTab = { 22 | uiConnect: (props: DaoSettingsTabUIProps) => ({ 23 | content: , 24 | name: "Settings", 25 | }), 26 | }; 27 | -------------------------------------------------------------------------------- /src/shared/lib/args/args-types/args-array.ts: -------------------------------------------------------------------------------- 1 | import { ArraySchema as _ArraySchema } from "yup"; 2 | 3 | declare module "yup" { 4 | interface ArraySchema {} 5 | } 6 | 7 | export { _ArraySchema as ArraySchema }; 8 | -------------------------------------------------------------------------------- /src/shared/lib/args/args-types/args-boolean.ts: -------------------------------------------------------------------------------- 1 | import { BooleanSchema as _BooleanSchema } from "yup"; 2 | 3 | declare module "yup" { 4 | interface BooleanSchema {} 5 | } 6 | 7 | export { _BooleanSchema as BooleanSchema }; 8 | -------------------------------------------------------------------------------- /src/shared/lib/args/args-types/args-mixed.ts: -------------------------------------------------------------------------------- 1 | import { MixedSchema as _MixedSchema } from "yup"; 2 | 3 | declare module "yup" { 4 | interface MixedSchema {} 5 | } 6 | 7 | class MixedSchema extends _MixedSchema {} 8 | 9 | export { MixedSchema }; 10 | -------------------------------------------------------------------------------- /src/shared/lib/args/args-types/args-number.ts: -------------------------------------------------------------------------------- 1 | import { NumberSchema as _NumberSchema } from "yup"; 2 | 3 | declare module "yup" { 4 | interface NumberSchema {} 5 | } 6 | 7 | export { _NumberSchema as NumberSchema }; 8 | -------------------------------------------------------------------------------- /src/shared/lib/args/args-types/args-object.ts: -------------------------------------------------------------------------------- 1 | import { addMethod, AnySchema, ObjectSchema as _ObjectSchema, reach } from "yup"; 2 | import { RetainOptions } from "../args-error"; 3 | 4 | declare module "yup" { 5 | interface ObjectSchema { 6 | requireAll(options?: Partial): this; 7 | retainAll(options?: Partial, retainOptions?: Partial): this; 8 | } 9 | } 10 | 11 | interface ApplyToAllOptions { 12 | ignore: string[]; 13 | } 14 | 15 | /** 16 | * call .require() on all children 17 | */ 18 | addMethod(_ObjectSchema, "requireAll", function requireAll(options?: Partial) { 19 | Object.entries(this.fields).forEach(([key, field]) => 20 | !options?.ignore?.includes(key) 21 | ? (this.fields[key] = 22 | (field as AnySchema).type === "object" 23 | ? (this.fields[key] as any).requireAll() 24 | : (this.fields[key] as AnySchema).required()) 25 | : void 0 26 | ); 27 | return this.required(); 28 | }); 29 | 30 | /** 31 | * call .retain() on all children 32 | */ 33 | addMethod( 34 | _ObjectSchema, 35 | "retainAll", 36 | function retainAll(options?: Partial, retainOptions?: Partial) { 37 | Object.entries(this.fields).forEach(([key, field]) => 38 | !options?.ignore?.includes(key) 39 | ? (this.fields[key] = 40 | (field as AnySchema).type === "object" 41 | ? (this.fields[key] as any).retainAll() 42 | : (this.fields[key] as any).retain(retainOptions)) 43 | : void 0 44 | ); 45 | 46 | return this.retain(); 47 | } 48 | ); 49 | 50 | /** 51 | * get subschema at path 52 | * @param schema 53 | * @param path 54 | * @returns 55 | */ 56 | function fields(schema: _ObjectSchema, path: string = ""): Record> { 57 | return reach(schema, path).fields; 58 | } 59 | 60 | export { _ObjectSchema as ObjectSchema, fields }; 61 | -------------------------------------------------------------------------------- /src/shared/lib/args/args.ts: -------------------------------------------------------------------------------- 1 | import { ArraySchema } from "./args-types/args-array"; 2 | import { BigSchema } from "./args-types/args-big"; 3 | import { BooleanSchema } from "./args-types/args-boolean"; 4 | import { MixedSchema } from "./args-types/args-mixed"; 5 | import { NumberSchema } from "./args-types/args-number"; 6 | import { ObjectSchema } from "./args-types/args-object"; 7 | import { StringSchema } from "./args-types/args-string"; 8 | 9 | export const args = { 10 | array: () => new ArraySchema(), 11 | big: () => new BigSchema(), 12 | boolean: () => new BooleanSchema(), 13 | mixed: () => new MixedSchema(), 14 | number: () => new NumberSchema(), 15 | object: () => new ObjectSchema(), 16 | string: () => new StringSchema(), 17 | }; 18 | 19 | console.log(args); 20 | -------------------------------------------------------------------------------- /src/shared/lib/call.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | 3 | export type Call = { 4 | address: string; 5 | actions: Array<{ 6 | func: string; 7 | args: TArgs; 8 | gas: string; 9 | depo: string; 10 | }>; 11 | }; 12 | 13 | export class CallError extends Error { 14 | taskId: string; 15 | 16 | constructor(message: string, taskId: string) { 17 | super(message); 18 | this.taskId = taskId; 19 | } 20 | } 21 | 22 | /** 23 | * transform a Call into a string 24 | * @param call 25 | */ 26 | const toString = (call: Call): string => JSON.stringify(call, null, " "); 27 | 28 | /** 29 | * transform a Call into JSON, produces a deep copy of the Call object 30 | * @param call 31 | */ 32 | const toJson = (call: Call): object => JSON.parse(JSON.stringify(call)); 33 | 34 | /** 35 | * transform a Call into JSON, with the args field encoded as base64 string 36 | * @param call 37 | */ 38 | const toBase64 = (call: Call): object => ({ 39 | address: call.address, 40 | actions: call.actions.map((action) => ({ 41 | ...action, 42 | args: Base64.encode(JSON.stringify(action.args)), 43 | })), 44 | }); 45 | 46 | export const fromCall = { 47 | toString, 48 | toJson, 49 | toBase64, 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/lib/contracts/generic.ts: -------------------------------------------------------------------------------- 1 | import { viewAccount } from "../wallet"; 2 | 3 | /** 4 | * check if there's a contract deployed on given NEAR address. 5 | * Accounts without contract have code_hash '11111111111111111111111111111111'. 6 | * 7 | * @param {string} address 8 | */ 9 | async function hasContract(address: string): Promise { 10 | const accountInfo = await viewAccount(address); 11 | const codeHash: string = accountInfo.code_hash; 12 | return codeHash !== "11111111111111111111111111111111"; 13 | } 14 | 15 | // TODO: method to list all available functions from a contract 16 | 17 | export { hasContract }; 18 | -------------------------------------------------------------------------------- /src/shared/lib/contracts/meta-pool.ts: -------------------------------------------------------------------------------- 1 | import { view } from "../wallet"; 2 | import { HumanReadableAccount } from "./staking-pool"; 3 | 4 | export type GetAccountInfoResult = { 5 | account_id: string; 6 | 7 | /// The available balance that can be withdrawn 8 | available: string; 9 | 10 | /// The amount of stNEAR owned (shares owned) 11 | st_near: string; 12 | ///stNEAR owned valued in NEAR 13 | valued_st_near: string; // st_near * stNEAR_price 14 | 15 | //META owned (including pending rewards) 16 | meta: string; 17 | //realized META (without pending rewards) 18 | realized_meta: string; 19 | 20 | /// The amount unstaked waiting for withdraw 21 | unstaked: string; 22 | 23 | /// The epoch height when the unstaked will be available 24 | unstaked_requested_unlock_epoch: string; 25 | /// How many epochs we still have to wait until unstaked_requested_unlock_epoch (epoch_unlock - env::epoch_height ) 26 | unstake_full_epochs_wait_left: number; 27 | ///if env::epoch_height()>=unstaked_requested_unlock_epoch 28 | can_withdraw: boolean; 29 | /// total amount the user holds in this contract: account.available + account.staked + current_rewards + account.unstaked 30 | total: string; 31 | 32 | //-- STATISTICAL DATA -- 33 | // User's statistical data 34 | // These fields works as a car's "trip meter". The user can reset them to zero. 35 | /// trip_start: (unix timestamp) this field is set at account creation, so it will start metering rewards 36 | trip_start: string; 37 | /// How many stnear the user had at "trip_start". 38 | trip_start_stnear: string; // OBSOLETE 39 | /// how much the user staked since trip start. always incremented 40 | trip_accum_stakes: string; 41 | /// how much the user unstaked since trip start. always incremented 42 | trip_accum_unstakes: string; 43 | /// to compute trip_rewards we start from current_stnear, undo unstakes, undo stakes and finally subtract trip_start_stnear 44 | /// trip_rewards = current_stnear + trip_accum_unstakes - trip_accum_stakes - trip_start_stnear; 45 | /// trip_rewards = current_stnear + trip_accum_unstakes - trip_accum_stakes - trip_start_stnear; 46 | trip_rewards: string; 47 | 48 | //Liquidity Pool 49 | nslp_shares: string; 50 | nslp_share_value: string; 51 | nslp_share_bp: number; //basis points, % user owned 52 | }; 53 | 54 | const FACTORY_ADDRESS_SELECTOR: Record = { 55 | mainnet: "meta-pool.near", 56 | testnet: "meta-v2.pool.testnet", 57 | }; 58 | 59 | export class MetaPool { 60 | static FACTORY_ADDRESS: string = FACTORY_ADDRESS_SELECTOR[window.NEAR_ENV]; 61 | address: string; 62 | 63 | constructor(daoAddress: string) { 64 | this.address = daoAddress; 65 | } 66 | 67 | async getAccount(accountId: string): Promise { 68 | return view(this.address, "get_account", { account_id: accountId }); 69 | } 70 | 71 | async getAccountInfo(accountId: string): Promise { 72 | return view(this.address, "get_account_info", { account_id: accountId }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/shared/lib/contracts/paras.ts: -------------------------------------------------------------------------------- 1 | import { view } from "../wallet"; 2 | import { args } from "../args/args"; 3 | 4 | const MARKETPLACE_ADDRESS_SELECTOR: Record = { 5 | mainnet: "marketplace.paras.near", 6 | testnet: "paras-marketplace-v2.testnet", 7 | }; 8 | 9 | const NFT_CONTRACT_ADDRESS_SELECTOR: Record = { 10 | mainnet: "x.paras.near", 11 | testnet: "paras-token-v2.testnet", 12 | }; 13 | 14 | // base URLs for Paras UI 15 | const UI_URL_SELECTOR: Record = { 16 | mainnet: "https://paras.id", 17 | testnet: "https://testnet.paras.id", 18 | }; 19 | 20 | type MarketDataJson = { 21 | owner_id: string; 22 | approval_id: string; 23 | nft_contract_id: string; 24 | token_id: string; 25 | ft_token_id: string; // "near" for NEAR token 26 | price: string; 27 | bids?: any; 28 | started_at?: string; 29 | ended_at?: string; 30 | end_price?: string; 31 | is_auction?: boolean; 32 | transaction_fee?: string; 33 | }; 34 | 35 | class Paras { 36 | static MARKETPLACE_ADDRESS: string = MARKETPLACE_ADDRESS_SELECTOR[window.NEAR_ENV]; 37 | static NFT_CONTRACT_ADDRESS: string = NFT_CONTRACT_ADDRESS_SELECTOR[window.NEAR_ENV]; 38 | static UI_BASE_URL: string = UI_URL_SELECTOR[window.NEAR_ENV]; 39 | 40 | static async getMarketData(nftContractId: string, tokenId: string): Promise { 41 | return view(this.MARKETPLACE_ADDRESS, "get_market_data", { 42 | nft_contract_id: nftContractId, 43 | token_id: tokenId, 44 | }).catch(() => null); 45 | } 46 | 47 | static getInfoFromListingUrl(url: string): { nftContractId: string; tokenId: string } | undefined { 48 | // create URL object from url 49 | let urlObj: URL; 50 | try { 51 | urlObj = new URL(url); 52 | } catch (e) { 53 | // input string isn't a valid URL 54 | return; 55 | } 56 | 57 | if (this.UI_BASE_URL === urlObj.origin) { 58 | const path: string[] = urlObj.pathname.split("/"); 59 | const info = path[2].split("::"); 60 | // check output validity: nftContractId is valid NEAR address 61 | if (info.length === 2 && args.string().address().isValidSync(info[0])) { 62 | const tokenIdEncoded = path.length === 4 ? path.pop()! : info.pop()!; 63 | return { nftContractId: info[0], tokenId: decodeURIComponent(tokenIdEncoded) }; 64 | } 65 | } 66 | } 67 | 68 | static isListingURLValid(urlString: string): boolean { 69 | return Boolean(this.getInfoFromListingUrl(urlString)); 70 | } 71 | } 72 | 73 | export { Paras }; 74 | export type { MarketDataJson }; 75 | -------------------------------------------------------------------------------- /src/shared/lib/contracts/token-farm.ts: -------------------------------------------------------------------------------- 1 | import type { FungibleTokenMetadata } from "../standards/fungibleToken"; 2 | import { view } from "../wallet"; 3 | 4 | const FACTORY_ADDRESS_SELECTOR: Record = { 5 | mainnet: "tkn.near", 6 | testnet: "tokens.testnet", 7 | }; 8 | 9 | export type TokenArgs = { 10 | owner_id: string; 11 | total_supply: string; 12 | metadata: FungibleTokenMetadata; 13 | }; 14 | 15 | export class TknFarm { 16 | static FACTORY_ADDRESS: string = FACTORY_ADDRESS_SELECTOR[window.NEAR_ENV]; 17 | // static CONTRACT_CODE_HASHES: string[] = CONTRACT_CODE_HASHES_SELECTOR[window.NEAR_ENV]; 18 | address: string; 19 | 20 | constructor(address: string) { 21 | this.address = address; 22 | } 23 | 24 | // /** 25 | // * check of given accountId is a TknFarm instance. 26 | // * uses code_hash of the contract deployed on accountId. 27 | // * 28 | // * @param accountId 29 | // */ 30 | // static async isTknFarm(accountId: string): Promise { 31 | // const accountInfo = await viewAccount(accountId); 32 | // const codeHash: string = accountInfo.code_hash; 33 | // return TknFarm.CONTRACT_CODE_HASHES.includes(codeHash); 34 | // } 35 | 36 | async getRequiredDeposit(args: TokenArgs, accountId: string): Promise { 37 | return view(this.address, "get_required_deposit", { args, account_id: accountId }); 38 | } 39 | 40 | async getToken(tokenId: string): Promise { 41 | return view(this.address, "get_token", { token_id: tokenId }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/lib/loader.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE } from "./persistent"; 2 | 3 | window.onbeforeunload = function (e) { 4 | STORAGE.save(); 5 | 6 | // e = e || window.event; 7 | 8 | // // For IE and Firefox prior to version 4 9 | // if (e) { 10 | // e.returnValue = 'Sure?'; 11 | // } 12 | 13 | // // For Safari 14 | // return 'Sure?'; 15 | }; 16 | 17 | export function saveFile(name: string, data: any) { 18 | const element = document.createElement("a"); 19 | const file = new Blob(data, { type: "text/plain" }); 20 | element.href = URL.createObjectURL(file); 21 | element.download = name; 22 | element.click(); 23 | } 24 | 25 | export function readFile(file: File, callback: (json: object) => any) { 26 | if (file.type !== "application/json") return; 27 | 28 | let reader = new FileReader(); 29 | reader.readAsText(file, "UTF-8"); 30 | 31 | reader.onload = (e_reader) => callback(JSON.parse(e_reader?.target?.result as string)); 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/lib/persistent.ts: -------------------------------------------------------------------------------- 1 | import { initialData } from "../../entities/task/config/initial-data"; 2 | import debounce from "lodash.debounce"; 3 | 4 | const STORAGE_KEY_ADDRESSES = "multicall_addresses"; 5 | const STORAGE_KEY_JSON = "multicall_json"; 6 | 7 | // Singleton for storing + persisting addresses and layout on local storage. 8 | class Persistent { 9 | // addresses relevant to multicall interactions. Initialize with empty strings. 10 | addresses: { user: string; multicall: string; dao: string } = { user: "", multicall: "", dao: "" }; 11 | // TODO: type layout 12 | layout: object = JSON.parse(JSON.stringify(initialData)); 13 | 14 | constructor() { 15 | // try initializing with values from local storage 16 | let storedAddresses = localStorage.getItem(STORAGE_KEY_ADDRESSES); 17 | this.addresses = storedAddresses ? JSON.parse(storedAddresses) : this.addresses; 18 | } 19 | 20 | setAddresses = debounce( 21 | // debounced function 22 | (newAddresses: { user?: string; multicall?: string; dao?: string }) => { 23 | this.addresses = { 24 | ...this.addresses, 25 | ...newAddresses, 26 | }; 27 | 28 | document.dispatchEvent( 29 | new CustomEvent("onaddressesupdated", { 30 | detail: { 31 | ...this.addresses, 32 | }, 33 | }) 34 | ); 35 | }, 36 | // delay in ms 37 | 100 38 | ); 39 | 40 | setLayout(newLayout: any) { 41 | this.layout = { 42 | ...this.layout, 43 | ...JSON.parse(JSON.stringify(newLayout)), 44 | }; 45 | 46 | document.dispatchEvent( 47 | new CustomEvent("onlayoutupdated", { 48 | detail: { 49 | ...this.layout, 50 | }, 51 | }) 52 | ); 53 | } 54 | 55 | save() { 56 | if (window.SIDEBAR.getPage() !== "app") return; 57 | 58 | localStorage.setItem(STORAGE_KEY_ADDRESSES, JSON.stringify(this.addresses)); 59 | localStorage.setItem(STORAGE_KEY_JSON, JSON.stringify(window.LAYOUT.toBase64(true))); 60 | // localStorage.setItem("multicall_layout", JSON.stringify(this.layout)); 61 | } 62 | 63 | load() { 64 | this.setAddresses(JSON.parse(localStorage.getItem(STORAGE_KEY_ADDRESSES) ?? "{}")); 65 | window.LAYOUT?.fromBase64(JSON.parse(localStorage.getItem(STORAGE_KEY_JSON) ?? "[]")); 66 | // this.setLayout(JSON.parse(localStorage.getItem("multicall_layout") ?? "{}")); 67 | } 68 | } 69 | 70 | const STORAGE = new Persistent(); 71 | 72 | export { STORAGE }; 73 | -------------------------------------------------------------------------------- /src/shared/lib/standards/multiFungibleToken.ts: -------------------------------------------------------------------------------- 1 | import { view } from "../wallet"; 2 | import type { StorageBalanceBounds } from "./storageManagement"; 3 | 4 | // Fungible token metadata follows NEP-148. See: https://nomicon.io/Standards/Tokens/FungibleToken/Metadata 5 | type MultiFungibleTokenMetadata = { 6 | spec: string; 7 | name: string; 8 | symbol: string; 9 | icon?: string | null; // optional 10 | reference?: string | null; // optional 11 | reference_hash?: string | null; // optional 12 | decimals: number; 13 | }; 14 | 15 | // Fungible token core follow NEP-141. See: https://nomicon.io/Standards/Tokens/FungibleToken/Core 16 | // Also implements NEP-145 for storage management. See: https://nomicon.io/Standards/StorageManagement 17 | class MultiFungibleToken { 18 | address: string; 19 | id: string; 20 | // needs initialization, but start with empty metadata 21 | metadata: MultiFungibleTokenMetadata = { spec: "", name: "", symbol: "", decimals: -1 }; 22 | // storage balance bounds. Needs initialization, but starts with "0" values 23 | // Users must have at least the min amount to receive tokens. 24 | storageBounds: StorageBalanceBounds = { min: "0", max: "0" }; 25 | // Token instance is ready when info (metadata...) are fetched & assigned correctly 26 | ready: boolean = false; 27 | 28 | // shouldn't be used directly, use init() instead 29 | constructor(tokenAddress: string, tokenId: string) { 30 | this.address = tokenAddress; 31 | this.id = tokenId; 32 | } 33 | 34 | // used to create and initialize a MultiFungibleToken instance 35 | static async init(tokenAddress: string, tokenId: string): Promise { 36 | // fetch token info and mark it ready 37 | const newToken = new MultiFungibleToken(tokenAddress, tokenId); 38 | const [metadata] = await Promise.all([ 39 | // on failure set metadata to default metadata (empty) 40 | newToken.mftMetadata().catch((err) => { 41 | return newToken.metadata; 42 | }), 43 | ]); 44 | newToken.metadata = metadata; 45 | // set ready to true if token info successfully got updated. 46 | if (newToken.metadata.decimals >= 0) { 47 | newToken.ready = true; 48 | } 49 | return newToken; 50 | } 51 | 52 | async mftMetadata(): Promise { 53 | return view(this.address, "mft_metadata", { token_id: this.id }); 54 | } 55 | } 56 | 57 | export { MultiFungibleToken }; 58 | -------------------------------------------------------------------------------- /src/shared/lib/types.d.ts: -------------------------------------------------------------------------------- 1 | export declare type AccountId = string; 2 | 3 | export declare type U128String = string; 4 | export declare type U64String = string; 5 | 6 | export declare type JsonString = string; 7 | export declare type Base64String = string; 8 | -------------------------------------------------------------------------------- /src/shared/lib/validation.ts: -------------------------------------------------------------------------------- 1 | const isUrl = (urlString: string): boolean => { 2 | try { 3 | return Boolean(new URL(urlString)); 4 | } catch { 5 | return false; 6 | } 7 | }; 8 | 9 | /** 10 | * Check if a string is a valid NEAR account id. 11 | * 12 | * @param accountId 13 | */ 14 | const isNearAccountId = (accountId: string): boolean => { 15 | // Regexp for NEAR account IDs. See: https://github.com/near/nearcore/blob/180e5dda991ad7bdbb389a931e84d24e31fb0674/core/account-id/src/lib.rs#L240 16 | const ACCOUNT_ID_REGEX: RegExp = /^(?=.{2,64}$)(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/; 17 | return ACCOUNT_ID_REGEX.test(accountId); 18 | }; 19 | 20 | /** 21 | * only returns true on integers & floats. Reject exponentials, scientific notations ... 22 | * 23 | * @param numberStr string of a number 24 | */ 25 | const isSimpleNumberStr = (numberStr: string): boolean => { 26 | const SIMPLE_NUM_REGEX: RegExp = /^\d+(\.\d+)?$/; 27 | return SIMPLE_NUM_REGEX.test(numberStr); 28 | }; 29 | 30 | function isDataURL(s: string): boolean { 31 | const DATA_URL_REGEX = /^(data:)([\w\/\+-]*)(;charset=[\w-]+|;base64){0,1},(.*)/gi; 32 | return !!s.match(DATA_URL_REGEX); 33 | } 34 | 35 | export const Validation = { isUrl, isNearAccountId, isSimpleNumberStr, isDataURL }; 36 | -------------------------------------------------------------------------------- /src/shared/lib/window.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkId } from "@near-wallet-selector/core"; 2 | import { Component } from "react"; 3 | 4 | import { Sidebar, Menu, Editor } from "../../widgets"; 5 | import { Task } from "../../entities"; 6 | import AppPage from "../../pages/app"; 7 | import { WalletComponent } from "../../entities/wallet/ui/wallet"; 8 | 9 | type CardInfo = { 10 | formData: object; 11 | showArgs: boolean; 12 | isEdited: boolean; 13 | options: object; 14 | }; 15 | 16 | type CardCopy = { 17 | from: string; 18 | to: string; 19 | payload?: Omit; 20 | }; 21 | 22 | declare global { 23 | interface Window { 24 | // Page components 25 | DAO_COMPONENT: Component; 26 | 27 | MENU: Menu; 28 | EDITOR: Editor; 29 | EXPORT: Component; 30 | 31 | LAYOUT: AppPage; 32 | SIDEBAR: Sidebar; 33 | 34 | // List of all mounted tasks 35 | TASKS: Array; 36 | 37 | // Temporary storage for moving and cloning cards 38 | TEMP: CardInfo | null; 39 | COPY: CardCopy | null; 40 | 41 | // Wallet definitions 42 | WALLET_COMPONENT: WalletComponent; 43 | NEAR_ENV: NetworkId; 44 | nearConfig: any; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/ui/design/button-group/button-group.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | 4 | .ButtonGroup { 5 | @include mixin.center-items(space-between, center); 6 | flex-flow: row nowrap; 7 | width: 100%; 8 | 9 | &--end { 10 | margin-top: auto; 11 | } 12 | 13 | &--start { 14 | margin-bottom: auto; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/ui/design/button-group/button-group.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLProps } from "react"; 3 | 4 | import "./button-group.scss"; 5 | 6 | export interface ButtonGroupProps extends HTMLProps { 7 | placement?: "auto" | "end" | "start"; 8 | } 9 | 10 | const _ButtonGroup = "ButtonGroup"; 11 | 12 | export const ButtonGroup = ({ children, className, placement = "auto" }: ButtonGroupProps) => ( 13 |
{children}
14 | ); 15 | -------------------------------------------------------------------------------- /src/shared/ui/design/button-group/index.ts: -------------------------------------------------------------------------------- 1 | export { ButtonGroup } from "./button-group"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/button/button.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | 4 | .Button { 5 | border-radius: size.$task-radius; 6 | padding: 0.25 * size.$gap size.$gap; 7 | font-size: size.$text; 8 | background-color: color.$light; 9 | 10 | &:disabled { 11 | background-color: color.$darkish; 12 | cursor: not-allowed; 13 | } 14 | 15 | &--success { 16 | background-color: color.$green; 17 | } 18 | 19 | &--error { 20 | background-color: color.$red; 21 | } 22 | 23 | &-label { 24 | text-transform: uppercase; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/shared/ui/design/button/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ButtonHTMLAttributes } from "react"; 3 | 4 | import "./button.scss"; 5 | 6 | interface ButtonProps extends Omit, "children"> { 7 | color?: "default" | "success" | "error"; 8 | label: string; 9 | noRender?: boolean; 10 | } 11 | 12 | const _Button = "Button"; 13 | 14 | export const Button = ({ className, color = "default", label = "Submit", noRender = false, ...props }: ButtonProps) => 15 | noRender ? null : ( 16 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/shared/ui/design/button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from "./button"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/context.ts: -------------------------------------------------------------------------------- 1 | export namespace DesignContext { 2 | export type Color = "blue" | "green" | "red" | "yellow"; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/ui/design/data-inspector/context.ts: -------------------------------------------------------------------------------- 1 | import { chromeLight } from "react-inspector"; 2 | 3 | export class ModuleContext { 4 | static readonly theme = { 5 | ...chromeLight, 6 | BASE_BACKGROUND_COLOR: "transparent", 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/ui/design/data-inspector/data-inspector-node.tsx: -------------------------------------------------------------------------------- 1 | import { ObjectName, ObjectPreview, ObjectRootLabel, ObjectValue } from "react-inspector"; 2 | 3 | interface ObjectLabelProps { 4 | data: string | object; 5 | isNonenumerable: boolean; 6 | name: string; 7 | } 8 | 9 | export const ObjectLabel = ({ name, data, isNonenumerable = false }: ObjectLabelProps) => ( 10 | 11 | {typeof name === "string" ? ( 12 | 16 | ) : ( 17 | 18 | )} 19 | 20 | {": "} 21 | {Object.keys(data).length === 0 ? Array.isArray(data) ? "[]" : "{}" : } 22 | 23 | ); 24 | 25 | interface DataInspectorNodeProps extends ObjectLabelProps { 26 | depth: number; 27 | expanded: boolean; 28 | } 29 | 30 | export const DataInspectorNode = ({ depth, name, data, isNonenumerable, expanded }: DataInspectorNodeProps) => 31 | depth === 0 ? : ; 32 | -------------------------------------------------------------------------------- /src/shared/ui/design/data-inspector/data-inspector.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/mixin"; 4 | @use "sass/font"; 5 | 6 | .DataInspector { 7 | text-align: start; 8 | 9 | &-label { 10 | color: darken(color.$blue, 30%); 11 | cursor: pointer; 12 | & > span { 13 | font-family: font.$text !important; 14 | } 15 | } 16 | 17 | &-body { 18 | @include mixin.no-scrollbar; 19 | margin-top: 0.5 * size.$gap; 20 | overflow-x: scroll; 21 | overflow-y: hidden; 22 | 23 | & * { 24 | font-size: size.$text; 25 | font-family: font.$code !important; 26 | } 27 | 28 | div > span { 29 | cursor: pointer; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/ui/design/data-inspector/data-inspector.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { MouseEvent, useCallback, useState, type ComponentProps } from "react"; 3 | import { ObjectInspector } from "react-inspector"; 4 | 5 | import { ModuleContext as ModuleContext } from "./context"; 6 | import { DataInspectorNode } from "./data-inspector-node"; 7 | import "./data-inspector.scss"; 8 | 9 | const _DataInspector = "DataInspector"; 10 | 11 | interface DataInspectorProps extends ComponentProps { 12 | classes?: { root?: string; body?: string; label?: string }; 13 | expanded?: boolean; 14 | label?: string; 15 | } 16 | 17 | export const DataInspector = ({ classes, expanded = false, expandLevel = 1, label, ...props }: DataInspectorProps) => { 18 | // TODO: Extract custom `
` element to separate component. 19 | 20 | const [rootExpanded, rootExpandedUpdate] = useState(expanded), 21 | dynamicLabel = rootExpanded ? "hide" : "show"; 22 | 23 | const rootExpansionToggle = useCallback( 24 | (event: MouseEvent) => { 25 | event.preventDefault(); 26 | rootExpandedUpdate(!rootExpanded); 27 | }, 28 | 29 | [rootExpanded, rootExpandedUpdate] 30 | ); 31 | 32 | return ( 33 |
37 | 41 | {label ?? dynamicLabel} 42 | 43 | 44 |
45 | 52 |
53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/shared/ui/design/data-inspector/index.ts: -------------------------------------------------------------------------------- 1 | export { DataInspector } from "./data-inspector"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/date-time-picker/date-time-picker.scss: -------------------------------------------------------------------------------- 1 | @use "sass/font"; 2 | @use "sass/color"; 3 | @use "sass/size"; 4 | 5 | .DateTimePicker { 6 | &-modal { 7 | border-radius: size.$task-radius !important; 8 | 9 | * { 10 | font-family: font.$text !important; 11 | font-size: size.$text !important; 12 | } 13 | 14 | .MuiYearPicker-root { 15 | button { 16 | padding: 0; 17 | } 18 | } 19 | 20 | .MuiClock-pin, 21 | .MuiClockPointer-root, 22 | .MuiClockPointer-thumb, 23 | .MuiPickersDay-root.Mui-selected, 24 | .PrivatePickersYear-yearButton.Mui-selected { 25 | background-color: color.$black !important; 26 | border-color: color.$black; 27 | } 28 | } 29 | 30 | &-input { 31 | .MuiInputBase-root { 32 | padding: 0; 33 | } 34 | .MuiInputAdornment-root { 35 | position: absolute; 36 | right: 0; 37 | .MuiIconButton-root { 38 | padding: 8px; 39 | margin: 6px; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/ui/design/date-time-picker/date-time-picker.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from "@mui/material"; 2 | import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon"; 3 | import { 4 | DateTimePicker as GenericDateTimePicker, 5 | type DateTimePickerProps as GenericDateTimePickerProps, 6 | } from "@mui/x-date-pickers/DateTimePicker"; 7 | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; 8 | import clsx from "clsx"; 9 | import { DateTime } from "luxon"; 10 | 11 | import "./date-time-picker.scss"; 12 | 13 | const _DateTimePicker = "DateTimePicker"; 14 | 15 | export interface DateTimePickerProps 16 | extends Omit< 17 | GenericDateTimePickerProps, 18 | "onChange" | "maxDateTime" | "minDateTime" | "renderInput" | "value" 19 | > { 20 | classes?: { root?: string; modal?: string; input?: string }; 21 | handleChange: (value: DateTime | null, keyboardInputValue: string | undefined) => void; 22 | maxDateTime: Date; 23 | minDateTime: Date; 24 | value: Date; 25 | } 26 | 27 | export const DateTimePicker = ({ 28 | classes, 29 | handleChange, 30 | maxDateTime, 31 | minDateTime, 32 | value, 33 | ...props 34 | }: DateTimePickerProps) => ( 35 | 36 | ( 45 | 49 | )} 50 | {...props} 51 | /> 52 | 53 | ); 54 | -------------------------------------------------------------------------------- /src/shared/ui/design/date-time-picker/index.ts: -------------------------------------------------------------------------------- 1 | export { DateTimePicker, type DateTimePickerProps } from "./date-time-picker"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/dialog/dialog.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/color"; 3 | @use "sass/font"; 4 | @use "sass/mixin"; 5 | 6 | .Dialog { 7 | .MuiPaper-root { 8 | min-width: size.$task-width; 9 | background-color: color.$black; 10 | color: color.$light-text; 11 | border-radius: size.$task-radius; 12 | font-size: size.$text; 13 | } 14 | 15 | &-title { 16 | &.MuiDialogTitle-root { 17 | font-size: size.$large-text; 18 | font-weight: 800; 19 | font-family: font.$text; 20 | padding: size.$gap; 21 | padding-bottom: 0.5 * size.$gap; 22 | } 23 | } 24 | 25 | &-content { 26 | display: flex; 27 | flex-flow: column nowrap; 28 | gap: 0.5 * size.$gap; 29 | 30 | .MuiTextField-root { 31 | width: 100%; 32 | 33 | p.Mui-error { 34 | color: color.$red; 35 | font-weight: 800; 36 | font-family: font.$text; 37 | font-size: size.$small-text; 38 | 39 | &::before { 40 | content: "Error: "; 41 | } 42 | } 43 | } 44 | } 45 | 46 | &-actions { 47 | &.MuiDialogActions-root { 48 | @include mixin.center-items(space-between, center); 49 | flex-flow: row nowrap; 50 | padding: size.$gap; 51 | padding-top: 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/ui/design/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog as MUIDialog, 3 | DialogTitle as MUIDialogTitle, 4 | DialogContent as MUIDialogContent, 5 | DialogActions as MUIDialogActions, 6 | } from "@mui/material"; 7 | import { clsx } from "clsx"; 8 | import { PropsWithChildren } from "react"; 9 | 10 | import { Button } from "../button"; 11 | 12 | import "./dialog.scss"; 13 | 14 | interface DialogProps extends PropsWithChildren { 15 | cancelRename?: string; 16 | className?: string; 17 | doneRename?: string; 18 | noCancel?: boolean; 19 | noSubmit?: boolean; 20 | onCancel?: VoidFunction; 21 | onClose?: VoidFunction; 22 | onSubmit?: VoidFunction; 23 | open: boolean; 24 | title: string; 25 | } 26 | 27 | const _Dialog = "Dialog"; 28 | 29 | export const Dialog = ({ 30 | cancelRename, 31 | children, 32 | className, 33 | doneRename, 34 | noCancel = false, 35 | noSubmit, 36 | onCancel, 37 | onClose, 38 | onSubmit, 39 | open, 40 | title, 41 | }: DialogProps) => ( 42 | 46 | {title} 47 | {children} 48 | 49 | 50 | 22 | ); 23 | 24 | export interface TabsItemPanelProps extends React.PropsWithChildren, React.HTMLAttributes {} 25 | 26 | export const TabsItemPanel = ({ children, className }: TabsItemPanelProps) => ( 27 |
{children}
28 | ); 29 | -------------------------------------------------------------------------------- /src/shared/ui/design/tabs/layout.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | @use "sass/color"; 4 | 5 | .Tabs-layout-buttonsPanel { 6 | @include mixin.center-items; 7 | z-index: 1; 8 | border-radius: size.$task-radius; 9 | height: size.$Tabs-layout-buttonsPanel-height; 10 | padding: 0; 11 | overflow: hidden; 12 | background-color: transparent; 13 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 14 | 15 | & > .Tabs-item-button { 16 | &:first-of-type { 17 | border-radius: size.$task-radius 0 0 size.$task-radius; 18 | } 19 | 20 | &:last-of-type { 21 | border-radius: 0 size.$task-radius size.$task-radius 0; 22 | } 23 | } 24 | } 25 | 26 | .Tabs-layout-contentSpace { 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/ui/design/tabs/layout.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | 4 | import "./layout.scss"; 5 | 6 | const _TabsLayout = "Tabs-layout"; 7 | 8 | interface TabsLayoutButtonsPanelProps extends React.PropsWithChildren, React.HTMLAttributes {} 9 | 10 | const TabsLayoutButtonsPanel = ({ children, className }: TabsLayoutButtonsPanelProps) => ( 11 |
{children}
12 | ); 13 | 14 | interface TabsLayoutContentSpaceProps extends React.PropsWithChildren, React.HTMLAttributes {} 15 | 16 | const TabsLayoutContentSpace = ({ children, className }: TabsLayoutContentSpaceProps) => ( 17 |
{children}
18 | ); 19 | 20 | export interface TabsLayoutProps extends React.PropsWithChildren { 21 | buttons: JSX.Element[]; 22 | classes?: { root?: string; buttonsPanel?: string; contentSpace?: string }; 23 | } 24 | 25 | export const TabsLayout = ({ buttons, children, classes }: TabsLayoutProps) => ( 26 |
27 | {buttons} 28 | {children} 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /src/shared/ui/design/tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; 3 | 4 | import { TabsItemButton, TabsItemButtonProps, TabsItemPanel } from "./item"; 5 | import { TabsLayout, TabsLayoutProps } from "./layout"; 6 | 7 | interface TabsProps extends Pick { 8 | activeItemIndexOverride?: number; 9 | activeItemSwitchOverride?: Dispatch>; 10 | classes?: TabsLayoutProps["classes"] & {}; 11 | items: { content: JSX.Element; lazy?: boolean; name: string }[]; 12 | } 13 | 14 | export const Tabs = ({ 15 | activeItemIndexOverride, 16 | activeItemSwitchOverride, 17 | classes, 18 | invertedColors, 19 | items, 20 | }: TabsProps) => { 21 | const [activeItemIndex, activeItemSwitch] = 22 | activeItemIndexOverride === undefined || activeItemSwitchOverride === undefined 23 | ? useState(0) 24 | : [activeItemIndexOverride, activeItemSwitchOverride]; 25 | 26 | const activeItemSwitchBond = useCallback( 27 | (itemIndex: number) => () => activeItemSwitch(itemIndex), 28 | [activeItemSwitch, items] 29 | ); 30 | 31 | return ( 32 | ( 35 | 42 | ))} 43 | > 44 | {items.map(({ content, lazy = false }, itemIndex) => 45 | activeItemIndex === itemIndex || !lazy ? ( 46 | 50 | {content} 51 | 52 | ) : null 53 | )} 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/shared/ui/design/text-input/index.ts: -------------------------------------------------------------------------------- 1 | export { TextInput, type TextInputProps, TextInputWithUnits, type TextInputWithUnitsProps } from "./text-input"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/tile/index.ts: -------------------------------------------------------------------------------- 1 | export { Tile, type TileProps } from "./tile"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/tile/tile.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/color"; 3 | 4 | .Tile { 5 | display: flex; 6 | flex-flow: column nowrap; 7 | position: relative; 8 | border-radius: size.$task-radius; 9 | padding: size.$gap * 0.5; 10 | gap: size.$gap * 0.5; 11 | background-color: color.$lightest; 12 | font-size: size.$small-text; 13 | 14 | &-header { 15 | display: flex; 16 | flex-flow: row nowrap; 17 | align-items: center; 18 | padding: size.$gap * 0.2 size.$gap * 0.2 size.$gap * 0.5; 19 | gap: size.$gap; 20 | 21 | &-text, 22 | svg { 23 | font-size: size.$large-text; 24 | } 25 | 26 | &-slot { 27 | &--start, 28 | &--end { 29 | .MuiButtonBase-root { 30 | padding: 4px; 31 | } 32 | } 33 | 34 | &--end { 35 | margin-left: auto; 36 | } 37 | } 38 | } 39 | 40 | &-subheader { 41 | margin-bottom: auto; 42 | } 43 | 44 | &-content { 45 | display: flex; 46 | flex-flow: column nowrap; 47 | position: relative; 48 | width: auto; 49 | height: 100%; 50 | overflow: hidden; 51 | } 52 | 53 | &-footer { 54 | margin-top: auto; 55 | } 56 | 57 | &-subheader, 58 | &-footer { 59 | display: flex; 60 | flex-flow: row nowrap; 61 | justify-content: center; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/shared/ui/design/tile/tile.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLAttributes, PropsWithChildren } from "react"; 3 | 4 | import { Placeholder } from "../placeholder"; 5 | 6 | import "./tile.scss"; 7 | 8 | const _Tile = "Tile"; 9 | 10 | export interface TileProps extends PropsWithChildren, Omit, "className"> { 11 | classes?: Partial< 12 | Record<"root" | "content" | "footer" | "header" | "subheader", HTMLAttributes["className"]> 13 | >; 14 | error?: Error | null; 15 | footer?: JSX.Element; 16 | heading?: string | null; 17 | headerSlots?: { start?: JSX.Element; end?: JSX.Element }; 18 | loading?: boolean; 19 | noData?: boolean; 20 | order?: "default" | "swapped"; 21 | subheader?: JSX.Element; 22 | } 23 | 24 | export const Tile = ({ 25 | children, 26 | classes, 27 | error, 28 | footer, 29 | heading, 30 | headerSlots, 31 | loading = false, 32 | noData = false, 33 | order = "default", 34 | subheader, 35 | }: TileProps) => ( 36 |
37 | 38 | {headerSlots?.start && {headerSlots?.start}} 39 | {heading &&

{heading}

} 40 | {headerSlots?.end && {headerSlots?.end}} 41 |
42 | 43 | {subheader &&
{subheader}
} 44 | 45 |
46 | {loading &&
} 47 | {!loading && noData && } 48 | 49 | {!loading && error && ( 50 | 54 | )} 55 | 56 | {!loading && !noData && !error && children} 57 |
58 | 59 | {footer &&
{footer}
} 60 |
61 | ); 62 | -------------------------------------------------------------------------------- /src/shared/ui/design/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { Tooltip } from "./tooltip"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/design/tooltip/tooltip.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | 3 | .Tooltip { 4 | &-title { 5 | font-size: size.$small-text; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/ui/design/tooltip/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as MuiTooltip, TooltipProps as MuiTooltipProps } from "@mui/material"; 2 | 3 | import "./tooltip.scss"; 4 | 5 | const _Tooltip = "Tooltip"; 6 | 7 | interface TooltipProps extends Omit { 8 | content: MuiTooltipProps["title"]; 9 | } 10 | 11 | export const Tooltip = ({ children, content, ...props }: TooltipProps) => ( 12 | 0 ? ( 15 |

{content}

16 | ) : ( 17 | content 18 | ) 19 | } 20 | {...props} 21 | > 22 | {children} 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /src/shared/ui/form/elements/form-control.tsx: -------------------------------------------------------------------------------- 1 | export { FormControl } from "@mui/material"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/form/elements/form-label.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | 4 | .FormLabel { 5 | &.MuiFormLabel-root { 6 | font-size: size.$text; 7 | color: color.$white; 8 | } 9 | 10 | &.is-focused.Mui-focused { 11 | color: color.$white; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/ui/form/elements/form-label.tsx: -------------------------------------------------------------------------------- 1 | import { FormLabel as GenericFormLabel, type FormLabelProps as GenericFromLabelProps } from "@mui/material"; 2 | 3 | import "./form-label.scss"; 4 | 5 | type FormLabelProps = Omit; 6 | 7 | const _FormLabel = "FormLabel"; 8 | 9 | export const FormLabel = ({ content, ...props }: FormLabelProps) => ( 10 | 14 | {content} 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/shared/ui/form/elements/form-radio-group.tsx: -------------------------------------------------------------------------------- 1 | export { RadioGroup as FormRadioGroup } from "@mui/material"; 2 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/checkbox-field.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/font"; 4 | @use "sass/mixin"; 5 | 6 | .CheckboxField { 7 | @include mixin.center-items(flex-start); 8 | flex-flow: row wrap; 9 | width: 100%; 10 | 11 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 12 | border-radius: 0.25 * size.$task-radius; 13 | 14 | // match Mui values 15 | margin-top: 8px; 16 | margin-bottom: 4px; 17 | 18 | &-checkbox { 19 | color: color.$light; 20 | &.Mui-checked { 21 | color: color.$green !important; 22 | } 23 | .MuiSvgIcon-root { 24 | font-size: size.$text; 25 | } 26 | } 27 | 28 | &-label > span { 29 | color: color.$white; 30 | font-family: font.$text; 31 | font-size: size.$small-text !important; 32 | line-height: size.$text !important; 33 | padding-top: 8.5px; 34 | padding-bottom: 8.5px; 35 | } 36 | 37 | &-formGroup { 38 | width: 100%; 39 | margin-left: 14px; 40 | } 41 | 42 | &.roundtop fieldset { 43 | border-top-left-radius: 0.75 * size.$task-radius !important; 44 | border-top-right-radius: 0.75 * size.$task-radius !important; 45 | } 46 | &.roundbottom fieldset { 47 | border-bottom-left-radius: 0.75 * size.$task-radius !important; 48 | border-bottom-right-radius: 0.75 * size.$task-radius !important; 49 | } 50 | 51 | &:hover, 52 | &:focus { 53 | border-color: color.$white; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/checkbox-field.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, CheckboxProps, FormControlLabel, FormGroup } from "@mui/material"; 2 | import clsx from "clsx"; 3 | import { useField } from "formik"; 4 | import "./checkbox-field.scss"; 5 | 6 | const _CheckboxField = "CheckboxField"; 7 | 8 | export type CheckboxFieldProps = Partial & { 9 | name: string; 10 | label: string; 11 | roundtop?: boolean; 12 | roundbottom?: boolean; 13 | className?: string; 14 | }; 15 | 16 | export const CheckboxField = ({ name, label, roundtop, roundbottom, className, ...props }: CheckboxFieldProps) => { 17 | const [field] = useField(name); 18 | return ( 19 |
29 | 30 | 40 | } 41 | /> 42 | 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/choice-field.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | @use "sass/color"; 4 | @use "sass/font"; 5 | 6 | .ChoiceField { 7 | display: flex; 8 | flex-flow: column nowrap; 9 | 10 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 11 | border-radius: 0.25 * size.$task-radius; 12 | 13 | // match Mui values 14 | margin: 8px 0 4px 0; 15 | padding: 8.5px 14px; 16 | 17 | color: color.$white; 18 | font-family: font.$text; 19 | font-size: size.$small-text; 20 | line-height: size.$text; 21 | 22 | &-input { 23 | @include mixin.center-items(flex-start, center); 24 | flex-flow: row nowrap; 25 | width: 100%; 26 | 27 | p { 28 | color: color.$light-text; 29 | font-weight: 800; 30 | font-size: size.$small-text; 31 | } 32 | 33 | button { 34 | height: 2em; 35 | margin: 0 0.75ch; 36 | padding: 0 1em; 37 | border-radius: size.$task-radius; 38 | border: 1px solid color.$light; 39 | font-size: size.$small-text; 40 | font-weight: 800; 41 | color: color.$light; 42 | background-color: rgba(color.$white, 0.1); 43 | 44 | &.selected { 45 | color: color.$green; 46 | border: 1px solid color.$green; 47 | } 48 | 49 | &:hover { 50 | background-color: rgba(color.$white, 0.2); 51 | } 52 | } 53 | } 54 | 55 | &-content > *:first-child { 56 | margin-top: 0.5 * size.$gap !important; 57 | } 58 | 59 | &.roundtop { 60 | border-top-left-radius: 0.75 * size.$task-radius !important; 61 | border-top-right-radius: 0.75 * size.$task-radius !important; 62 | } 63 | &.roundbottom { 64 | border-bottom-left-radius: 0.75 * size.$task-radius !important; 65 | border-bottom-right-radius: 0.75 * size.$task-radius !important; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/choice-field.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { useField } from "formik"; 3 | import React, { useEffect, useState } from "react"; 4 | import "./choice-field.scss"; 5 | 6 | const _ChoiceField = "ChoiceField"; 7 | 8 | type Options = { 9 | ids: string[]; 10 | choose: (id: string) => void; 11 | add: (id: string) => void; 12 | remove: (id: string) => void; 13 | toggle: (id: string) => void; 14 | isActive: (id: string) => boolean; 15 | }; 16 | 17 | type ChoiceFieldProps = { 18 | children: (options: Options) => React.ReactNode; 19 | show: (ids: string[]) => React.ReactNode; 20 | name: string; 21 | initial?: string[]; 22 | roundtop?: boolean; 23 | roundbottom?: boolean; 24 | }; 25 | 26 | export const ChoiceField = ({ roundbottom, roundtop, children, show, initial, name, ...props }: ChoiceFieldProps) => { 27 | const [field, meta, helper] = useField(name); 28 | const [ids, setIds] = useState(initial ?? []); 29 | useEffect(() => { 30 | helper.setValue(ids); 31 | }, [ids]); 32 | const choose = (id: string) => setIds((prevIds) => [id]); 33 | const add = (id: string) => setIds((prevIds) => [...prevIds, id]); 34 | const remove = (id: string) => setIds((prevIds) => prevIds.filter((x) => x !== id)); 35 | const toggle = (id: string) => (ids.includes(id) ? remove(id) : add(id)); 36 | const isActive = (id: string) => ids.includes(id); 37 | return ( 38 |
44 |
45 | {children({ ids, choose, add, remove, toggle, isActive })} 46 |
47 |
{show(ids)}
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/file-field.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { useField } from "formik"; 3 | import { TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from "@mui/material"; 4 | import "./file-field.scss"; 5 | 6 | const _FileField = "FileField"; 7 | 8 | type FileFieldProps = MuiTextFieldProps & { 9 | name: string; 10 | roundtop?: boolean; 11 | roundbottom?: boolean; 12 | accept: React.HTMLProps["accept"]; 13 | className?: string; 14 | onChangeHandler?: (event: React.FormEvent) => any; 15 | }; 16 | 17 | export const FileField = ({ 18 | roundbottom, 19 | roundtop, 20 | name, 21 | className, 22 | accept, 23 | onChangeHandler, 24 | ...props 25 | }: FileFieldProps) => { 26 | const [field, meta, helper] = useField(name); 27 | return ( 28 |
38 | { 51 | helper.setValue((e.currentTarget as HTMLInputElement).files?.[0] ?? null); 52 | onChangeHandler?.(e); 53 | }, 54 | }} 55 | {...props} 56 | /> 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/form-radio.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | 4 | .FormRadio { 5 | &-button.MuiButtonBase-root { 6 | color: color.$white; 7 | 8 | &.Mui-checked { 9 | color: color.$white; 10 | } 11 | } 12 | 13 | &-label.MuiFormControlLabel-label { 14 | color: color.$white; 15 | font-size: size.$text; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/form-radio.tsx: -------------------------------------------------------------------------------- 1 | import { FormControlLabel, Radio } from "@mui/material"; 2 | import clsx from "clsx"; 3 | import { ComponentProps } from "react"; 4 | 5 | import "./form-radio.scss"; 6 | 7 | export interface FormRadioProps extends Omit, "control"> {} 8 | 9 | const _FormRadio = "FormRadio"; 10 | 11 | export const FormRadio = ({ className, ...props }: FormRadioProps) => ( 12 | } 15 | {...props} 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/info-field.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/font"; 4 | 5 | .InfoField { 6 | width: 100%; 7 | 8 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 9 | border-radius: 0.25 * size.$task-radius; 10 | 11 | // match Mui values 12 | margin: 8px 0 4px 0; 13 | 14 | color: color.$white; 15 | font-family: font.$text; 16 | font-size: size.$small-text; 17 | line-height: size.$text; 18 | 19 | &-content { 20 | display: flex; 21 | flex-flow: column; 22 | padding: 8.5px 14px; 23 | img, 24 | svg { 25 | margin: auto; 26 | max-height: size.$task-height; 27 | max-width: 100%; 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | &.roundtop { 33 | border-top-left-radius: 0.75 * size.$task-radius !important; 34 | border-top-right-radius: 0.75 * size.$task-radius !important; 35 | } 36 | &.roundbottom { 37 | border-bottom-left-radius: 0.75 * size.$task-radius !important; 38 | border-bottom-right-radius: 0.75 * size.$task-radius !important; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/info-field.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import "./info-field.scss"; 3 | 4 | const _InfoField = "InfoField"; 5 | 6 | type InfoFieldProps = React.PropsWithChildren & { 7 | roundtop?: boolean; 8 | roundbottom?: boolean; 9 | }; 10 | 11 | export const InfoField = ({ roundbottom, roundtop, ...props }: InfoFieldProps) => { 12 | return ( 13 |
19 |
{props.children}
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/select-field.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem } from "@mui/material"; 2 | import clsx from "clsx"; 3 | import { TextField, TextFieldProps } from "./text-field"; 4 | 5 | const _SelectField = "SelectField"; 6 | 7 | export type SelectFieldProps = TextFieldProps & { 8 | options: string[]; 9 | }; 10 | 11 | export const SelectField = ({ options, ...props }: SelectFieldProps) => { 12 | return ( 13 |
14 | 19 | {options.map((o) => ( 20 | 24 | {o} 25 | 26 | ))} 27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/text-field.tsx: -------------------------------------------------------------------------------- 1 | import { Autocomplete, TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from "@mui/material"; 2 | import clsx from "clsx"; 3 | import { useField } from "formik"; 4 | import "./text-field.scss"; 5 | 6 | const _TextField = "TextField"; 7 | 8 | export type TextFieldProps = Partial & { 9 | name: string; 10 | autocomplete?: string[]; 11 | roundtop?: boolean; 12 | roundbottom?: boolean; 13 | className?: string; 14 | }; 15 | 16 | export const TextField = ({ 17 | name, 18 | autocomplete, 19 | roundtop, 20 | roundbottom, 21 | className, 22 | children, 23 | ...props 24 | }: TextFieldProps) => { 25 | const [field, meta, helper] = useField(name); 26 | return ( 27 |
37 | {!!autocomplete ? ( 38 | { 44 | helper.setValue(value); 45 | }} 46 | renderInput={(params) => ( 47 | { 58 | helper.setTouched(true); 59 | field.onChange(e); 60 | }} 61 | {...props} 62 | > 63 | {children} 64 | 65 | )} 66 | /> 67 | ) : ( 68 | { 78 | helper.setTouched(true); 79 | field.onChange(e); 80 | }} 81 | {...props} 82 | > 83 | {children} 84 | 85 | )} 86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/unit-field.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/mixin"; 3 | 4 | .UnitField { 5 | @include mixin.center-items(center, flex-start); 6 | flex-flow: row nowrap; 7 | width: 100%; 8 | gap: 0.5 * size.$gap; 9 | &-text { 10 | flex: 2 1 auto; 11 | } 12 | &-unit { 13 | flex: 1 0 25%; 14 | .MuiTextField-root, 15 | .MuiInputBase-root { 16 | width: 100% !important; 17 | } 18 | } 19 | &.roundtop { 20 | .UnitField-text fieldset { 21 | border-top-left-radius: 0.75 * size.$task-radius !important; 22 | } 23 | .UnitField-unit fieldset { 24 | border-top-right-radius: 0.75 * size.$task-radius !important; 25 | } 26 | } 27 | &.roundbottom { 28 | .UnitField-text fieldset { 29 | border-bottom-left-radius: 0.75 * size.$task-radius !important; 30 | } 31 | .UnitField-unit fieldset { 32 | border-bottom-right-radius: 0.75 * size.$task-radius !important; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/ui/form/fields/unit-field.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { TextField, TextFieldProps } from "./text-field"; 3 | import { SelectField, SelectFieldProps } from "./select-field"; 4 | import "./unit-field.scss"; 5 | 6 | const _UnitField = "UnitField"; 7 | 8 | type UnitFieldProps = { 9 | textProps?: Omit; 10 | unitProps?: Omit; 11 | name: string; 12 | unit: string; 13 | label: string; 14 | options: string[]; 15 | roundtop?: boolean; 16 | roundbottom?: boolean; 17 | }; 18 | 19 | export const UnitField = ({ name, unit, label, options, ...props }: UnitFieldProps) => { 20 | return ( 21 |
27 | 33 | 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/shared/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | /** Elements */ 2 | export { FormControl } from "./elements/form-control"; 3 | export { FormLabel } from "./elements/form-label"; 4 | export { FormRadioGroup } from "./elements/form-radio-group"; 5 | 6 | /** Fields */ 7 | export { CheckboxField } from "./fields/checkbox-field"; 8 | export { ChoiceField } from "./fields/choice-field"; 9 | export { FormRadio } from "./fields/form-radio"; 10 | export { FileField } from "./fields/file-field"; 11 | export { InfoField } from "./fields/info-field"; 12 | export { SelectField } from "./fields/select-field"; 13 | export { TextField } from "./fields/text-field"; 14 | export { UnitField } from "./fields/unit-field"; 15 | -------------------------------------------------------------------------------- /src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.svg"; 3 | declare module "*.jpeg"; 4 | declare module "*.jpg"; 5 | -------------------------------------------------------------------------------- /src/widgets/builder/builder.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/size"; 3 | @use "sass/color"; 4 | @use "sass/base"; 5 | 6 | .Builder { 7 | @include mixin.center-items(flex-start); 8 | gap: size.$gap; 9 | 10 | &-form { 11 | width: 100%; 12 | } 13 | &-droppable { 14 | flex: 1; 15 | width: 100%; 16 | position: relative; 17 | } 18 | &-selector { 19 | @include mixin.no-scrollbar; 20 | position: absolute; 21 | left: 0; 22 | z-index: 2; 23 | width: size.$task-width; 24 | max-height: 100%; 25 | overflow-y: scroll; 26 | border-radius: size.$task-radius; 27 | .column-container { 28 | width: 100%; 29 | height: 100%; 30 | padding: 0 !important; 31 | margin: 0 !important; 32 | background-color: transparent !important; 33 | .tasks-wrapper { 34 | flex: 1 1 0; 35 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 36 | .task-wrapper:last-of-type { 37 | margin-bottom: 0 !important; 38 | } 39 | .icon { 40 | display: none; 41 | } 42 | .task-container { 43 | outline: none !important; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/widgets/builder/index.ts: -------------------------------------------------------------------------------- 1 | export { Builder } from "./builder"; 2 | -------------------------------------------------------------------------------- /src/widgets/column/column.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/color"; 3 | @use "sass/mixin"; 4 | 5 | .column-container { 6 | flex: 0 0 size.$task-width; 7 | @include mixin.center-items(flex-start, center); 8 | flex-flow: column nowrap; 9 | position: relative; 10 | min-height: calc(100% - 2 * size.$gap); 11 | width: size.$task-width; 12 | padding: 0 size.$gap; 13 | margin-right: size.$gap; 14 | margin-top: size.$gap; 15 | background-color: rgba(color.$lightest, 0.25); 16 | border-radius: size.$task-radius; 17 | .drag-handle { 18 | @include mixin.icon; 19 | line-height: size.$large-text; 20 | padding: 0.5 * size.$gap; 21 | scale: 3 1; 22 | } 23 | .delete-column { 24 | @include mixin.icon; 25 | position: absolute; 26 | top: 0.5 * size.$gap; 27 | right: 0.5 * size.$gap; 28 | cursor: pointer; 29 | } 30 | &:not(:last-of-type) > .add-column { 31 | opacity: 0; 32 | } 33 | &:last-of-type > .add-column { 34 | @include mixin.icon; 35 | position: absolute; 36 | top: 0.5 * size.$gap; 37 | right: -1 * size.$gap; 38 | transition: opacity 200ms ease-in-out; 39 | cursor: pointer; 40 | } 41 | .tasks-wrapper { 42 | flex: 1; 43 | @include mixin.center-items(flex-start); 44 | width: 100%; 45 | flex-flow: column nowrap; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/widgets/column/index.ts: -------------------------------------------------------------------------------- 1 | export { Column } from "./column"; 2 | -------------------------------------------------------------------------------- /src/widgets/dialogs-layer/index.ts: -------------------------------------------------------------------------------- 1 | import { DialogsLayer } from "./ui/dialogs-layer"; 2 | 3 | export { DialogsLayer }; 4 | -------------------------------------------------------------------------------- /src/widgets/dialogs-layer/ui/dialogs-layer.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLogin } from "../../../features/external-login"; 2 | 3 | export const DialogsLayer = () => ( 4 | <> 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/widgets/editor/editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import hash from "object-hash"; 3 | import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; 4 | import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined"; 5 | import { Formik } from "formik"; 6 | import "./editor.scss"; 7 | 8 | export class Editor extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | editingID: null, 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | window.EDITOR = this; 19 | } 20 | 21 | edit(taskID) { 22 | this.setState({ editingID: taskID }); 23 | window.TASKS.forEach((t) => t.instance.current.onEditFocus(taskID)); 24 | } 25 | 26 | render() { 27 | const { editingID } = this.state; 28 | 29 | const editing = window?.TASKS?.find((t) => t.id === editingID)?.instance.current; 30 | const keyObj = { 31 | card: editingID, 32 | formData: editing?.state.formData, 33 | }; 34 | 35 | return editing ? ( 36 | editing.validateForm(values)} 39 | onSubmit={() => {}} 40 | key={hash(keyObj, { algorithm: "md5", encoding: "base64" })} 41 | > 42 | <> 43 | 44 | {/*

45 | {editing.state.formData.name}, {editingID} 46 |

*/} 47 | 48 |
49 | ) : ( 50 |
51 | 52 |

53 | Click the icon in the top right corner of a task to start 54 | editing! 55 |

56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/widgets/editor/editor.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | @use "sass/mixin"; 3 | @use "sass/base"; 4 | @use "sass/color"; 5 | 6 | .placeholder { 7 | @include mixin.center-items; 8 | padding: 0 size.$gap; 9 | color: color.$light-text; 10 | font-size: size.$text; 11 | text-align: center; 12 | .huge-icon { 13 | font-size: 2 * size.$huge-text; 14 | margin-top: -2 * size.$gap; 15 | margin-bottom: size.$gap; 16 | } 17 | h3 > .icon { 18 | font-size: size.$large-text; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/widgets/editor/index.ts: -------------------------------------------------------------------------------- 1 | export { Editor } from "./editor"; 2 | -------------------------------------------------------------------------------- /src/widgets/export/index.ts: -------------------------------------------------------------------------------- 1 | export { Export } from "./export"; 2 | -------------------------------------------------------------------------------- /src/widgets/index.ts: -------------------------------------------------------------------------------- 1 | export { Column } from "./column/column.jsx"; 2 | export { DialogsLayer } from "./dialogs-layer"; 3 | export { Menu } from "./menu/menu.jsx"; 4 | export { Builder } from "./builder/builder.jsx"; 5 | export { Editor } from "./editor/editor.jsx"; 6 | export { Export } from "./export"; 7 | export { SettingsEditor, type SettingsEditorModule } from "./settings-editor"; 8 | export { Sidebar } from "./sidebar/sidebar.jsx"; 9 | export { TokenBalances, type TokenBalancesModule } from "./token-balances"; 10 | -------------------------------------------------------------------------------- /src/widgets/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { Menu } from "./menu"; 2 | -------------------------------------------------------------------------------- /src/widgets/menu/menu.jsx: -------------------------------------------------------------------------------- 1 | import Icon from "@mui/material/Icon"; 2 | import clsx from "clsx"; 3 | import { Component } from "react"; 4 | 5 | import { Tabs } from "../../shared/ui/design"; 6 | import { Builder } from "../builder/builder.jsx"; 7 | import { Editor } from "../editor/editor.jsx"; 8 | import { Export } from "../export/export"; 9 | import "./menu.scss"; 10 | 11 | export class Menu extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | expanded: false, 17 | activeTabIndex: 0, 18 | }; 19 | 20 | document.addEventListener("onaddressesupdated", () => this.forceUpdate()); 21 | } 22 | 23 | componentDidMount() { 24 | window.MENU = this; 25 | } 26 | 27 | activeTabSwitch = (newTabIndex) => this.setState({ activeTabIndex: newTabIndex }); 28 | 29 | render() { 30 | const { activeTabIndex, expanded } = this.state; 31 | 32 | /** Usually global parameter */ 33 | const LAYOUT = this.props.layout; 34 | 35 | return ( 36 |
37 |
38 | 55 | ), 56 | }, 57 | { 58 | name: "Edit", 59 | content: , 60 | }, 61 | { 62 | name: "Export", 63 | content: , 64 | }, 65 | ]} 66 | /> 67 | 68 |
69 | { 72 | LAYOUT.setExpanded(!expanded); 73 | this.setState({ expanded: !expanded }); 74 | }} 75 | > 76 | navigate_before 77 | 78 |
79 |
80 |
81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/widgets/menu/menu.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/size"; 3 | @use "sass/mixin"; 4 | @use "sass/animation"; 5 | 6 | .Menu { 7 | position: fixed; 8 | top: size.$gap; 9 | right: size.$gap; 10 | width: size.$menu-width; 11 | height: calc(100% - 2 * size.$gap); 12 | min-width: calc(size.$task-width + 2 * size.$gap); 13 | background-color: color.$black; 14 | box-shadow: 1px 10px 20px 0 rgba(0, 0, 0, 0.2); 15 | border-radius: 1.75 * size.$task-radius; 16 | z-index: 2; 17 | transition: width animation.$menu-expand-time ease-out; 18 | &-tabs { 19 | height: calc(100% - size.$gap); 20 | &-buttonsPanel { 21 | box-shadow: inset 0px 0px 30px 0px rgba(color.$white, 0.1); 22 | border-radius: size.$task-radius; 23 | margin: size.$gap; 24 | width: calc(100% - 2 * size.$gap); 25 | 26 | .Tabs-item-button { 27 | flex: 1; 28 | } 29 | } 30 | &-contentSpace { 31 | .Tabs-item-panel { 32 | &.is-active { 33 | flex-flow: column; 34 | gap: size.$gap; 35 | height: calc(100% - size.$Tabs-layout-buttonsPanel-height); 36 | 37 | & > div { 38 | display: flex; 39 | flex-flow: column nowrap; 40 | height: calc(100% - size.$gap); 41 | width: calc(100% - (2 * size.$gap)); 42 | padding: 0 size.$gap size.$gap size.$gap; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | &--expanded { 49 | width: animation.$menu-expand-width; 50 | transition: width animation.$menu-expand-time ease-out; 51 | } 52 | } 53 | 54 | .toggle-size { 55 | position: absolute; 56 | left: calc(-0.5 * (size.$large-text + size.$gap)); 57 | top: calc(50% - 0.5 * size.$large-text); 58 | width: calc(size.$large-text + size.$gap); 59 | transition: transform animation.$menu-expand-time linear; 60 | 61 | .icon { 62 | scale: 1.2; 63 | } 64 | 65 | &.expand { 66 | transform: scaleX(1); 67 | 68 | .icon { 69 | @include mixin.icon; 70 | font-size: size.$large-text; 71 | } 72 | } 73 | 74 | &.collapse { 75 | transform: scaleX(-1); 76 | .icon { 77 | @include mixin.light-icon; 78 | font-size: size.$large-text; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/widgets/settings-editor/context.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from "react"; 2 | 3 | import { SchedulingSettingsChange, TokenWhitelistChange } from "../../features"; 4 | import { MulticallSettingsDiff, Multicall } from "../../shared/lib/contracts/multicall"; 5 | import { SputnikDAO } from "../../shared/lib/contracts/sputnik-dao"; 6 | 7 | export namespace SettingsEditor { 8 | export interface Inputs extends HTMLProps { 9 | adapters: { dao: SputnikDAO; multicall: Multicall }; 10 | } 11 | 12 | export type Diff = MulticallSettingsDiff; 13 | 14 | export type ProposalDescription = Parameters[0]; 15 | } 16 | 17 | export class ModuleContext { 18 | public static readonly DiffKey = { 19 | ...SchedulingSettingsChange.DiffKey, 20 | ...TokenWhitelistChange.DiffKey, 21 | }; 22 | 23 | public static readonly DiffMeta = { 24 | ...SchedulingSettingsChange.DiffMeta, 25 | ...TokenWhitelistChange.DiffMeta, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/widgets/settings-editor/index.ts: -------------------------------------------------------------------------------- 1 | import { ModuleContext, SettingsEditor as SettingsEditorModule } from "./context"; 2 | import { SettingsEditorUI } from "./ui/settings-editor"; 3 | 4 | export class SettingsEditor extends ModuleContext { 5 | static UI = SettingsEditorUI; 6 | } 7 | 8 | export { type SettingsEditorModule }; 9 | -------------------------------------------------------------------------------- /src/widgets/settings-editor/ui/se-proposal-form.scss: -------------------------------------------------------------------------------- 1 | @use "sass/color"; 2 | @use "sass/mixin"; 3 | @use "sass/size"; 4 | 5 | .SettingsEditor-proposalForm { 6 | grid-area: SEProposalForm; 7 | gap: size.$gap * 0.4; 8 | 9 | &-hint { 10 | @include mixin.center-items(); 11 | text-align: center; 12 | font-size: size.$large-text; 13 | } 14 | 15 | &-summary { 16 | display: none; 17 | flex-flow: column nowrap; 18 | gap: inherit; 19 | 20 | &-entry { 21 | display: inherit; 22 | flex-flow: row wrap; 23 | align-items: center; 24 | gap: inherit; 25 | padding: 0 size.$gap * 0.4; 26 | 27 | &-description { 28 | white-space: nowrap; 29 | line-height: 1; 30 | } 31 | 32 | &-data { 33 | display: inherit; 34 | flex-flow: row wrap; 35 | gap: 1rem; 36 | padding: 0; 37 | line-height: 1; 38 | list-style: none; 39 | 40 | &-chip { 41 | border-radius: size.$task-radius; 42 | padding: 0.5rem 0.6rem; 43 | 44 | &--blue { 45 | background-color: color.$blue; 46 | } 47 | 48 | &--green { 49 | background-color: color.$green; 50 | } 51 | 52 | &--red { 53 | background-color: color.$red; 54 | } 55 | 56 | &--yellow { 57 | background-color: color.$yellow; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | &-submit { 65 | display: none; 66 | justify-content: end; 67 | gap: inherit; 68 | } 69 | 70 | &-hint, 71 | &-submit { 72 | flex-flow: inherit; 73 | height: inherit; 74 | } 75 | 76 | &.is-inEditMode &-hint { 77 | display: none; 78 | } 79 | 80 | &.is-inEditMode &-summary { 81 | display: inherit; 82 | } 83 | 84 | &.is-inEditMode &-submit { 85 | display: inherit; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/widgets/settings-editor/ui/settings-editor.scss: -------------------------------------------------------------------------------- 1 | @use "sass/size"; 2 | 3 | .SettingsEditor { 4 | grid-area: SettingsEditor; 5 | display: grid; 6 | grid-template-columns: 1fr 1fr; 7 | grid-template-rows: 1fr 1fr; 8 | gap: size.$gap; 9 | height: 100%; 10 | 11 | grid-template-areas: 12 | "tokenWhitelist jobsSettings" 13 | "tokenWhitelist SEProposalForm"; 14 | 15 | &-admins { 16 | grid-area: admins; 17 | display: none; 18 | } 19 | 20 | &-jobsSettings { 21 | grid-area: jobsSettings; 22 | } 23 | 24 | &-tokenWhitelist { 25 | grid-area: tokenWhitelist; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/widgets/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { Sidebar } from "./sidebar"; 2 | -------------------------------------------------------------------------------- /src/widgets/sidebar/sidebar.scss: -------------------------------------------------------------------------------- 1 | @use "sass/mixin"; 2 | @use "sass/base"; 3 | @use "sass/color"; 4 | @use "sass/size"; 5 | 6 | .sidebar-wrapper { 7 | position: fixed; 8 | display: flex; 9 | flex-flow: column nowrap; 10 | height: 100%; 11 | width: size.$sidebar-width; 12 | background-color: color.$black; 13 | box-shadow: 1px 10px 20px 0 rgba(0, 0, 0, 0.2); 14 | z-index: 2; 15 | } 16 | 17 | .sidebar-container { 18 | @include mixin.center-items(flex-start); 19 | flex: 1; 20 | flex-flow: column nowrap; 21 | 22 | .title { 23 | position: relative; 24 | cursor: pointer; 25 | padding: calc(size.$gap - 0.5 * size.$large-text) 0; 26 | 27 | .logo { 28 | display: block; 29 | color: color.$light-text; 30 | font-size: 1.5 * size.$large-text; 31 | } 32 | 33 | .env { 34 | visibility: hidden; 35 | position: absolute; 36 | left: 60%; 37 | top: 50%; 38 | 39 | .icon { 40 | color: color.$yellow; 41 | font-size: size.$text; 42 | } 43 | } 44 | 45 | .env[env="testnet"] { 46 | visibility: visible; 47 | } 48 | } 49 | 50 | nav { 51 | display: flex; 52 | flex-flow: column nowrap; 53 | font-size: size.$text; 54 | 55 | a { 56 | text-align: center; 57 | line-height: size.$sidebar-width; 58 | vertical-align: middle; 59 | width: size.$sidebar-width; 60 | height: size.$sidebar-width; 61 | font-size: size.$text; 62 | color: color.$light-text !important; 63 | 64 | &.active { 65 | color: color.$text !important; 66 | background-color: color.$white; 67 | } 68 | 69 | &:not(.active):hover { 70 | background-color: rgba(color.$light, 0.1); 71 | } 72 | } 73 | } 74 | 75 | hr { 76 | width: 40%; 77 | } 78 | 79 | img { 80 | padding: 0.25 * size.$sidebar-width; 81 | width: 0.5 * size.$sidebar-width; 82 | height: 0.5 * size.$sidebar-width; 83 | fill: color.$light; 84 | cursor: pointer; 85 | opacity: 0.25; 86 | 87 | &:hover { 88 | opacity: 1; 89 | } 90 | } 91 | } 92 | 93 | .sidebar-button { 94 | &.MuiButtonBase-root { 95 | padding: 0; 96 | } 97 | 98 | .MuiSvgIcon-root { 99 | color: color.$light; 100 | padding: 0.25 * size.$sidebar-width; 101 | font-size: 1.5 * size.$large-text; 102 | cursor: pointer; 103 | 104 | &:hover { 105 | background-color: rgba(color.$light, 0.1); 106 | } 107 | } 108 | } 109 | 110 | .extras { 111 | margin-top: auto; 112 | .socials { 113 | @include mixin.center-items(space-around); 114 | } 115 | .legal-disclaimer { 116 | font-size: size.$small-text; 117 | max-width: 25ch; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/widgets/token-balances/context.ts: -------------------------------------------------------------------------------- 1 | import { NEARTokenModule, FTModule } from "../../entities"; 2 | 3 | export namespace TokenBalances { 4 | export interface Inputs extends NEARTokenModule.Inputs, FTModule.Inputs { 5 | className?: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/widgets/token-balances/index.ts: -------------------------------------------------------------------------------- 1 | import { TokenBalancesUI } from "./ui/token-balances"; 2 | import { TokenBalances as TokenBalancesModule } from "./context"; 3 | 4 | class TokenBalances { 5 | static UI = TokenBalancesUI; 6 | } 7 | 8 | export { TokenBalances, type TokenBalancesModule }; 9 | -------------------------------------------------------------------------------- /src/widgets/token-balances/ui/token-balances.scss: -------------------------------------------------------------------------------- 1 | @use "sass/font"; 2 | 3 | .TokenBalances { 4 | grid-area: TokenBalances; 5 | 6 | .TableRow-content--compact { 7 | &:not(&:first-of-type) { 8 | span:last-of-type { 9 | font-family: font.$code; 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/widgets/token-balances/ui/token-balances.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | import { Tile, Scrollable, Table } from "../../../shared/ui/design"; 4 | import { FT, NEARToken } from "../../../entities"; 5 | import { type TokenBalances } from "../context"; 6 | 7 | import "./token-balances.scss"; 8 | 9 | const _TokenBalances = "TokenBalances"; 10 | 11 | export const TokenBalancesUI = ({ className, adapters }: TokenBalances.Inputs) => { 12 | const nearTokenBalances = NEARToken.balancesRender({ adapters }), 13 | fungibleTokenBalances = FT.balances({ adapters }); 14 | 15 | return ( 16 | 20 | {(nearTokenBalances ?? fungibleTokenBalances) && ( 21 | 22 |
27 | 28 | )} 29 | 30 | {(!nearTokenBalances || !fungibleTokenBalances) &&
} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["node_modules/@types", "src/types"], 4 | "target": "es6", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "jsx": "react-jsx", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "noImplicitAny": true, 20 | "noImplicitThis": true, 21 | "strictNullChecks": true 22 | }, 23 | "include": ["src", "near-config.ts"] 24 | } 25 | --------------------------------------------------------------------------------