├── .browserslistrc
├── .editorconfig
├── .env
├── .github
├── ISSUE_TEMPLATE
│ ├── BUG_REPORT.yml
│ └── FEATURE_REQUEST.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── histoire.config.js
├── index.html
├── main.js
├── package.json
├── postcss.config.js
├── preload.js
├── public
├── favicon-temp.svg
└── manifest.json
├── src
├── App.vue
├── assets
│ ├── connectors
│ │ ├── coinbase.png
│ │ ├── gnosis.png
│ │ ├── metamask.png
│ │ ├── portis.png
│ │ ├── starknet.png
│ │ └── walletconnect.png
│ ├── fonts
│ │ ├── Calibre-Medium-Custom.woff2
│ │ ├── Calibre-Semibold-Custom.woff2
│ │ ├── SpaceMono-Bold.woff2
│ │ └── SpaceMono-Regular.woff2
│ ├── grid-dark.svg
│ ├── grid-light.svg
│ ├── icons
│ │ ├── discord.svg
│ │ ├── github.svg
│ │ └── x.svg
│ └── logo.svg
├── components
│ ├── App.vue
│ ├── Block
│ │ ├── Actions.vue
│ │ ├── ContactPicker.vue
│ │ ├── CreationConfirmation.vue
│ │ ├── Delegates.vue
│ │ ├── Execution.vue
│ │ ├── ExecutionEditable.vue
│ │ ├── InfiniteScroller.vue
│ │ ├── NftPicker.vue
│ │ ├── SpaceFormController.vue
│ │ ├── SpaceFormNetwork.vue
│ │ ├── SpaceFormProfile.vue
│ │ ├── SpaceFormStrategies.vue
│ │ ├── SpaceFormValidation.vue
│ │ ├── SpaceFormVoting.vue
│ │ ├── StrategiesConfigurator.vue
│ │ └── TokenPicker.vue
│ ├── Container.vue
│ ├── ExecutionButton.vue
│ ├── Label.vue
│ ├── Layout.vue
│ ├── Link.vue
│ ├── Markdown.vue
│ ├── MarkdownEditor.vue
│ ├── Modal
│ │ ├── Account.vue
│ │ ├── Delegate.vue
│ │ ├── DelegationConfig.vue
│ │ ├── Drafts.vue
│ │ ├── EditContact.vue
│ │ ├── EditSpace.vue
│ │ ├── EditStrategy.vue
│ │ ├── LinkWalletConnect.vue
│ │ ├── PendingTransactions.vue
│ │ ├── SendNft.vue
│ │ ├── SendToken.vue
│ │ ├── Timeline.vue
│ │ ├── Transaction.vue
│ │ ├── Votes.vue
│ │ └── VotingPower.vue
│ ├── Nav.vue
│ ├── NftPreview.vue
│ ├── Notifications.vue
│ ├── PendingTransactionsIndicator.vue
│ ├── Preview.vue
│ ├── Proposal.vue
│ ├── ProposalStatus.vue
│ ├── ProposalStatusIcon.vue
│ ├── ProposalsList.vue
│ ├── Results.vue
│ ├── S
│ │ ├── Base.vue
│ │ ├── IAddress.vue
│ │ ├── IArray.vue
│ │ ├── IBoolean.vue
│ │ ├── IDuration.vue
│ │ ├── INumber.vue
│ │ ├── IObject.vue
│ │ ├── ISelect.vue
│ │ ├── IStamp.vue
│ │ ├── IStampCover.vue
│ │ ├── IString.vue
│ │ └── IText.vue
│ ├── Sidebar.vue
│ ├── SpaceAvatar.vue
│ ├── SpaceCover.vue
│ ├── SpaceItem.vue
│ ├── Stamp.vue
│ ├── StrategyButton.vue
│ ├── Topnav.vue
│ ├── Transaction.vue
│ ├── Ui
│ │ ├── Alert.vue
│ │ ├── Button.story.vue
│ │ ├── Button.vue
│ │ ├── Counter.vue
│ │ ├── Dropdown.vue
│ │ ├── DropdownItem.vue
│ │ ├── Editable.vue
│ │ ├── Loading.vue
│ │ ├── Modal.vue
│ │ ├── Select.vue
│ │ └── Tooltip.vue
│ ├── Upload.vue
│ ├── Vote.vue
│ └── VotingPowerIndicator.vue
├── composables
│ ├── useAccount.ts
│ ├── useActions.ts
│ ├── useApp.ts
│ ├── useApps.ts
│ ├── useBalances.ts
│ ├── useDelegates.ts
│ ├── useEditor.ts
│ ├── useFavicon.ts
│ ├── useMarkdownEditor.ts
│ ├── useMixpanel.ts
│ ├── useModal.ts
│ ├── useNfts.ts
│ ├── useResolve.ts
│ ├── useRouteParser.ts
│ ├── useScrollMonitor.ts
│ ├── useSpaces.ts
│ ├── useTitle.ts
│ ├── useTreasury.ts
│ ├── useUserSkin.ts
│ ├── useWalletConnect.ts
│ ├── useWalletConnectTransaction.ts
│ └── useWeb3.ts
├── helpers
│ ├── __snapshots__
│ │ └── transactions.test.ts.snap
│ ├── abis.ts
│ ├── alchemy
│ │ ├── index.ts
│ │ └── types.ts
│ ├── argentx.ts
│ ├── auth.ts
│ ├── call.ts
│ ├── connectors.ts
│ ├── constants.ts
│ ├── ens.ts
│ ├── etherscan.ts
│ ├── execution.json
│ ├── mana.ts
│ ├── multicaller.ts
│ ├── networks.json
│ ├── pin.ts
│ ├── provider.ts
│ ├── resolver.ts
│ ├── stamp.ts
│ ├── tenderly.ts
│ ├── transactions.test.ts
│ ├── transactions.ts
│ ├── utils.test.ts
│ ├── utils.ts
│ └── validation.ts
├── histoire-setup.ts
├── main.ts
├── networks
│ ├── common
│ │ ├── constants.ts
│ │ ├── graphqlApi
│ │ │ ├── highlight.ts
│ │ │ ├── index.ts
│ │ │ ├── queries.ts
│ │ │ └── types.ts
│ │ └── helpers.ts
│ ├── evm
│ │ ├── actions.ts
│ │ ├── constants.ts
│ │ ├── helpers.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── offchain
│ │ ├── api
│ │ │ ├── index.ts
│ │ │ ├── queries.ts
│ │ │ └── types.ts
│ │ ├── constants.ts
│ │ └── index.ts
│ ├── starknet
│ │ ├── actions.ts
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ └── provider.ts
│ └── types.ts
├── router.ts
├── stores
│ ├── contacts.ts
│ ├── meta.ts
│ ├── proposals.ts
│ ├── spaces.ts
│ ├── ui.ts
│ └── users.ts
├── style.scss
├── types.ts
└── views
│ ├── App.vue
│ ├── Apps.vue
│ ├── Create.vue
│ ├── Editor.vue
│ ├── Explore.vue
│ ├── Home.vue
│ ├── Proposal.vue
│ ├── Proposal
│ ├── Overview.vue
│ └── Votes.vue
│ ├── Settings.vue
│ ├── Settings
│ ├── Contacts.vue
│ └── Spaces.vue
│ ├── Space.vue
│ ├── Space
│ ├── Delegates.vue
│ ├── EditSettings.vue
│ ├── Overview.vue
│ ├── Proposals.vue
│ ├── Search.vue
│ ├── Settings.vue
│ └── Treasury.vue
│ └── User.vue
├── tailwind.config.js
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = LF
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | VITE_ENABLED_NETWORKS=
2 | VITE_ETH_RPC_URL=https://rpc.snapshotx.xyz
3 | VITE_MANA_URL=https://mana.pizza
4 | VITE_HIGHLIGHT_URL=
5 | VITE_IPFS_GATEWAY=snapshot.4everland.link
6 | VITE_INFURA_API_KEY=46a5dd9727bf48d4a132672d3f376146
7 | VITE_ALCHEMY_API_KEY=ombBQyf580z-jx2EVQgJu4eTjePU-a2z
8 | VITE_GA_MEASUREMENT_ID=G-8MQS50MVZX
9 | VITE_TENDERLY_ACCESS_KEY=8-1V1FGLxFqhs75T7bUs4lK1BaPOnfcT
10 | VITE_MIXPANEL_TOKEN=47b43858cf6fe9376f8deddda52cbaaf
11 | VITE_WC_PROJECT_ID=b191e242d2f2163c99bcf9d44297e946
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_REPORT.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: File a bug report
3 | title: '[BUG] - '
4 | labels: ['bug-report']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for making Snapshot awesome for everyone.
9 | - type: input
10 | id: title
11 | attributes:
12 | label: Briefly describe the bug.
13 | description: A clear and concise description of what the bug is.
14 | placeholder: ex. Unable to vote using metamask wallet on chrome.
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: reproduce
19 | attributes:
20 | label: How can we reproduce the bug?
21 | placeholder: Steps to reproduce the behaviour.
22 | value: |
23 | 1. Go to '...'
24 | 2. Click on '....'
25 | 3. Scroll down to '....'
26 | 4. See error
27 | validations:
28 | required: true
29 | - type: textarea
30 | id: expectation
31 | attributes:
32 | label: What is the expected behaviour?
33 | description: A clear and concise description of what you expected to happen.
34 | placeholder: ex. I should be able to vote using metamask wallet on chrome.
35 | validations:
36 | required: false
37 | - type: textarea
38 | id: screenshots
39 | attributes:
40 | label: Can you attach screenshots?
41 | description: Please attach all the screenshots that can help us visually see the problem.
42 | placeholder: You can drag-and-drop the image, copy paste the image in the field below.
43 | validations:
44 | required: false
45 | - type: textarea
46 | id: device
47 | attributes:
48 | label: Can you provide your device specific details below?
49 | value: |
50 | 1. Device Type - [ex. Desktop or Mobile]
51 | 2. Device OS - [ex. Android, MacOS, Windows, iOS]
52 | 3. OS Version - [ex. iOS 11, Windows 10]
53 | 4. Browser - [ex. chrome, safari]
54 | 5. Browser Version - [ex. 101]
55 | 6. Wallet - [metamask, portis, walletconnect]
56 | 7. Space - [ex. ENS, Gitcoin]
57 | 8. Any additional information we should know -
58 | validations:
59 | required: false
60 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | title: '[NEW FEATURE] - '
4 | labels: ['feature-request']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for making Snapshot awesome for everyone.
9 | - type: input
10 | id: title
11 | attributes:
12 | label: Briefly describe the feature.
13 | description: A clear and concise description of the feature you are requesting.
14 | validations:
15 | required: true
16 | - type: textarea
17 | id: problem
18 | attributes:
19 | label: Which problem is this feature trying to solve?
20 | description: A clear description of the problem or frustration that will be solved with this feature.
21 | placeholder: ex. I'm always frustrated when I am viewing the proposal metrics
22 | validations:
23 | required: true
24 | - type: textarea
25 | id: solution
26 | attributes:
27 | label: What is the expected solution?
28 | description: A clear and concise description of what you expected to happen.
29 | placeholder: ex. Having a chart to read the votes on the proposal will make it very easy to read the proposal metrics.
30 | validations:
31 | required: true
32 | - type: textarea
33 | id: alternatives
34 | attributes:
35 | label: How do you currently handle this problem?
36 | description: A clear and concise description of any alternative solutions or features you've considered.
37 | validations:
38 | required: false
39 | - type: textarea
40 | id: additional
41 | attributes:
42 | label: Anything else you'd like to mention?
43 | description: Please mention any technical details, screenshots, mock-ups that you might have for us to better understand the feature.
44 | validations:
45 | required: false
46 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Summary
2 |
3 |
4 |
5 | Closes: #
6 |
7 | ### How to test
8 |
9 | 1.
10 |
11 | ### To-Do
12 |
13 | - [ ]
14 |
15 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push]
3 |
4 | jobs:
5 | build-test:
6 | runs-on: ubuntu-20.04
7 | steps:
8 | - uses: actions/checkout@v3
9 | - uses: actions/setup-node@v3
10 | with:
11 | node-version: '18'
12 | cache: 'yarn'
13 | - run: yarn --frozen-lockfile
14 | - run: yarn build
15 | - run: yarn typecheck
16 | - run: yarn lint:nofix
17 | - run: yarn test
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /coverage
5 | .yalc
6 | components.d.ts
7 | auto-imports.d.ts
8 | .eslintrc-auto-import.json
9 |
10 | # local env files
11 | .env.local
12 | .env.*.local
13 |
14 | # Log files
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 | pnpm-debug.log*
19 |
20 | # Editor directories and files
21 | .idea
22 | .vscode
23 | *.suo
24 | *.ntvs*
25 | *.njsproj
26 | *.sln
27 | *.sw?
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Snapshot Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/snapshot-labs/sx-ui/actions/workflows/test.yml)
2 | [](https://discord.snapshot.org/)
3 |
4 | # Snapshot X
5 |
6 | An open source interface for Snapshot X protocol.
7 |
8 | **[Website](https://snapshotx.xyz)**
9 |
10 | **[Documentation](https://obytejs.com)**
11 |
12 | ## Project setup
13 |
14 | ```
15 | yarn
16 | ```
17 |
18 | ### Compiles and hot-reloads for development
19 |
20 | ```
21 | yarn dev
22 | ```
23 |
24 | ### Compiles and minifies for production
25 |
26 | ```
27 | yarn build
28 | ```
29 |
30 | ### Lints and fixes files
31 |
32 | ```
33 | yarn lint
34 | ```
35 |
36 | ### Runs tests
37 |
38 | ```
39 | yarn test
40 | ```
41 |
42 | ### Verifies TypeScript code
43 |
44 | ```
45 | yarn typecheck
46 | ```
47 |
48 | ## Development guide
49 |
50 | This project uses `goerli` and `testnet-2` Starknet networks. Make sure that your Metamask/ArgentX is
51 | configured for those networks.
52 |
53 | If you need testnet funds you can use:
54 |
55 | - [PoW faucet](https://goerli-faucet.pk910.de) to acquire ETH on goerli.
56 | - [`0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6`](https://goerli.etherscan.io/address/0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6#writeContract) to wrap ETH to WETH.
57 | - [`0xAEA4513378Eb6023CF9cE730a26255D0e3F075b9`](https://goerli.etherscan.io/address/0xAEA4513378Eb6023CF9cE730a26255D0e3F075b9#writeProxyContract) to move ETH to Starknet testnet-2.
58 |
59 | If you want to test proposals that verify your WETH balance on Starknet proposals proofs have to be computed on L2.
60 | This is done manually currently.
61 |
62 | To do it:
63 |
64 | - Take some block on goerli at which you have your desired amount of WETH
65 | - Visit `https://mana.pizza/fossil/send/BLOCK_NUM_1` where BLOCK_NUM_1 is block you want to send to L2
66 | - Visit `https://mana.pizza/fossil/process/BLOCK_NUM_2` where BLOCK_NUM_2 is the block you used above **minus 1**
67 |
68 | If you need to modify services that are used by sx-ui you can specify them in `.env` file or applicable
69 | file in `./src/networks`.
70 |
71 | ## License
72 |
73 | Snapshot is open-sourced software licensed under the © [MIT license](LICENSE).
74 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset']
3 | };
4 |
--------------------------------------------------------------------------------
/histoire.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'histoire';
2 | import { HstVue } from '@histoire/plugin-vue';
3 |
4 | export default defineConfig({
5 | plugins: [HstVue()],
6 | setupFile: './src/histoire-setup.ts',
7 | theme: {
8 | defaultColorScheme: 'light',
9 | favicon: './public/favicon-temp.svg',
10 | title: 'Tune UI',
11 | colors: {
12 | gray: {
13 | 50: '#FBFBFB',
14 | 100: '#FBFBFB',
15 | 200: '#FBFBFB',
16 | 300: '#EDEDED',
17 | 400: '#EDEDED',
18 | 500: '#A09FA4',
19 | 600: '#A09FA4',
20 | 700: '#1C1B20',
21 | 750: '#29282E',
22 | 800: '#29282E',
23 | 850: '#29282E',
24 | 900: '#29282E',
25 | 950: '#29282E'
26 | },
27 | primary: {
28 | 50: '#EDEDED',
29 | 100: '#EDEDED',
30 | 200: '#EDEDED',
31 | 300: '#f3b04e',
32 | 400: '#f3b04e',
33 | 500: '#f3b04e',
34 | 600: '#f3b04e',
35 | 700: '#f3b04e',
36 | 800: '#f3b04e',
37 | 900: '#29282E'
38 | }
39 | }
40 | },
41 | backgroundPresets: []
42 | });
43 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Snapshot X
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | const path = require('path');
3 |
4 | function createWindow() {
5 | const win = new BrowserWindow({
6 | width: 400, // 357
7 | height: 660, // 600
8 | webPreferences: {
9 | preload: path.join(__dirname, 'preload.js')
10 | }
11 | });
12 |
13 | win.loadFile('dist/index.html');
14 | }
15 |
16 | app.whenReady().then(() => {
17 | createWindow();
18 |
19 | app.on('activate', () => {
20 | if (BrowserWindow.getAllWindows().length === 0) {
21 | createWindow();
22 | }
23 | });
24 | });
25 |
26 | app.on('window-all-closed', () => {
27 | if (process.platform !== 'darwin') {
28 | app.quit();
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sx-ui",
3 | "version": "0.1.0",
4 | "license": "MIT",
5 | "main": "main.js",
6 | "repository": "snapshot-labs/sx-ui",
7 | "scripts": {
8 | "dev": "vite --port=8080",
9 | "build": "vite build",
10 | "story:dev": "histoire dev",
11 | "story:build": "histoire build",
12 | "lint:nofix": "eslint \"./src/**/*.{ts,vue,json}\"",
13 | "lint": "yarn lint:nofix --fix",
14 | "typecheck": "vue-tsc --noEmit",
15 | "test": "vitest run",
16 | "test:watch": "vitest",
17 | "electron:start": "electron .",
18 | "electron:build": "ELECTRON=true yarn build"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "@snapshot-labs/vue",
23 | "./.eslintrc-auto-import.json"
24 | ]
25 | },
26 | "prettier": "@snapshot-labs/prettier-config",
27 | "dependencies": {
28 | "@apollo/client": "^3.8.4",
29 | "@argent/get-starknet": "^6.4.7",
30 | "@braintree/sanitize-url": "^6.0.4",
31 | "@ensdomains/eth-ens-namehash": "^2.0.15",
32 | "@ethersproject/abi": "^5.7.0",
33 | "@ethersproject/address": "^5.7.0",
34 | "@ethersproject/bignumber": "^5.7.0",
35 | "@ethersproject/constants": "^5.7.0",
36 | "@ethersproject/contracts": "^5.7.0",
37 | "@ethersproject/hash": "^5.7.0",
38 | "@ethersproject/providers": "^5.7.2",
39 | "@ethersproject/units": "^5.7.0",
40 | "@floating-ui/vue": "^1.0.2",
41 | "@headlessui/vue": "^1.7.16",
42 | "@openzeppelin/merkle-tree": "^1.0.5",
43 | "@snapshot-labs/eslint-config-vue": "^0.1.0-beta.13",
44 | "@snapshot-labs/lock": "^0.2.0",
45 | "@snapshot-labs/pineapple": "^1.1.0",
46 | "@snapshot-labs/prettier-config": "^0.1.0-beta.7",
47 | "@snapshot-labs/sx": "^0.1.0-beta.58",
48 | "@vueuse/core": "^10.4.1",
49 | "@walletconnect/core": "^2.11.0",
50 | "@walletconnect/types": "^2.11.0",
51 | "@walletconnect/utils": "^2.11.0",
52 | "@walletconnect/web3wallet": "^1.10.0",
53 | "ajv": "^8.12.0",
54 | "ajv-formats": "^2.1.1",
55 | "buffer": "^6.0.3",
56 | "dayjs": "^1.11.10",
57 | "electron": "^26.2.3",
58 | "events": "^3.3.0",
59 | "graphql": "^16.8.1",
60 | "graphql-tag": "^2.12.6",
61 | "ipfs-http-client": "^60.0.1",
62 | "js-sha3": "^0.9.2",
63 | "lodash.set": "^4.3.2",
64 | "mixpanel-browser": "^2.47.0",
65 | "object-hash": "^3.0.0",
66 | "pinia": "^2.1.6",
67 | "remarkable": "^2.0.1",
68 | "scrollmonitor": "^1.2.11",
69 | "starknet": "5.25.0",
70 | "stream-browserify": "^3.0.0",
71 | "unplugin-auto-import": "^0.16.6",
72 | "util": "^0.12.5",
73 | "vue": "^3.4.15",
74 | "vue-router": "^4.2.5",
75 | "vuedraggable": "^4.1.0"
76 | },
77 | "devDependencies": {
78 | "@histoire/plugin-vue": "^0.17.4",
79 | "@iconify-json/heroicons-outline": "^1.1.7",
80 | "@iconify-json/heroicons-solid": "^1.1.8",
81 | "@rollup/plugin-inject": "^5.0.3",
82 | "@types/mixpanel-browser": "^2.47.4",
83 | "@types/node": "^20.7.1",
84 | "@types/remarkable": "^2.0.4",
85 | "@vitejs/plugin-vue": "^5.0.3",
86 | "autoprefixer": "^10.4.16",
87 | "eslint": "^8.56.0",
88 | "histoire": "^0.17.9",
89 | "husky": "^8.0.3",
90 | "postcss": "^8.4.30",
91 | "prettier": "^3.0.3",
92 | "rollup-plugin-visualizer": "^5.9.2",
93 | "sass": "^1.68.0",
94 | "tailwindcss": "^3.3.3",
95 | "typescript": "^5.2.2",
96 | "unplugin-icons": "^0.17.0",
97 | "unplugin-vue-components": "^0.25.2",
98 | "vite": "^5.0.12",
99 | "vitest": "^1.2.2",
100 | "vue-tsc": "^1.8.27"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('DOMContentLoaded', () => {
2 | const replaceText = (selector, text) => {
3 | const element = document.getElementById(selector);
4 | if (element) element.innerText = text;
5 | };
6 |
7 | for (const type of ['chrome', 'node', 'electron']) {
8 | replaceText(`${type}-version`, process.versions[type]);
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/public/favicon-temp.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Snapshot X",
3 | "name": "Snapshot X",
4 | "description": "Where decisions get made",
5 | "iconPath": "favicon-temp.svg",
6 | "start_url": ".",
7 | "display": "standalone",
8 | "theme_color": "#000000",
9 | "background_color": "#ffffff"
10 | }
11 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
76 |
77 |
85 |
91 |
92 |
93 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
118 |
--------------------------------------------------------------------------------
/src/assets/connectors/coinbase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/coinbase.png
--------------------------------------------------------------------------------
/src/assets/connectors/gnosis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/gnosis.png
--------------------------------------------------------------------------------
/src/assets/connectors/metamask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/metamask.png
--------------------------------------------------------------------------------
/src/assets/connectors/portis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/portis.png
--------------------------------------------------------------------------------
/src/assets/connectors/starknet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/starknet.png
--------------------------------------------------------------------------------
/src/assets/connectors/walletconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/connectors/walletconnect.png
--------------------------------------------------------------------------------
/src/assets/fonts/Calibre-Medium-Custom.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/fonts/Calibre-Medium-Custom.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Calibre-Semibold-Custom.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/fonts/Calibre-Semibold-Custom.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/SpaceMono-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/fonts/SpaceMono-Bold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/SpaceMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snapshot-labs/sx-ui/5344667bcffb88c73f7c6a0260425283c7537e2c/src/assets/fonts/SpaceMono-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/grid-dark.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/grid-light.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
14 |
--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Block/ContactPicker.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/Block/Execution.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/Block/InfiniteScroller.vue:
--------------------------------------------------------------------------------
1 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/components/Block/NftPicker.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/Block/SpaceFormController.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 | Controller
39 |
40 | (model = v)"
45 | />
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/components/Block/SpaceFormNetwork.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
Space network
21 |
22 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/Block/SpaceFormStrategies.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
{{ title }}
24 |
25 | {{ description }}
26 |
27 | (model = value)"
32 | />
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/Block/SpaceFormVoting.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 | Voting settings
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/Container.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/ExecutionButton.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
25 |
26 |
27 |
35 |
--------------------------------------------------------------------------------
/src/components/Label.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Link.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/MarkdownEditor.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
23 |
24 |
25 |
31 |
32 |
33 |
39 |
40 |
41 |
47 |
48 |
49 |
55 |
56 |
57 |
70 |
71 |
72 |
73 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/components/Modal/Account.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
62 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/components/Modal/Drafts.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
51 | {{ proposal.title || 'Untitled' }}
52 | #{{ proposal.key }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
There isn't any drafts yet!
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/components/Modal/EditContact.vue:
--------------------------------------------------------------------------------
1 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Confirm
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/components/Modal/EditStrategy.vue:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
53 |
54 | Edit strategy
55 |
56 |
57 |
58 |
59 |
60 |
61 |
68 |
69 |
70 |
71 |
77 |
78 |
84 |
85 |
86 |
87 | Confirm
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/components/Modal/PendingTransactions.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | Pending transactions
19 |
20 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/Modal/Timeline.vue:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
91 |
92 |
93 |
94 | {{ _t(state.value) }}
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/components/Modal/VotingPower.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 | Your voting power
31 |
32 |
33 |
34 |
39 |
40 |
45 |
46 | {{ _n(Number(strategy.value) / 10 ** finalDecimals) }} {{ votingPowerSymbol }}
47 |
48 |
49 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/components/Nav.vue:
--------------------------------------------------------------------------------
1 |
80 |
81 |
82 |
89 |
90 |
91 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/src/components/NftPreview.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/Notifications.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
15 | {{ notification.message }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/PendingTransactionsIndicator.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
13 |
18 |
19 | {{ uiStore.pendingTransactions.length }}
20 |
21 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/Preview.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
![]()
34 |
35 |
36 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/ProposalStatus.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
27 |
31 |
35 |
39 |
40 |
44 | {{ titles[state] }}
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/ProposalStatusIcon.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/ProposalsList.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
48 | {{ route.linkTitle }}
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/components/S/Base.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | {{ error }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/S/IAddress.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
22 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/S/IArray.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Add
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/S/IBoolean.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/S/IDuration.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
55 |
56 |
57 |
70 |
--------------------------------------------------------------------------------
/src/components/S/INumber.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/S/IObject.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
71 |
72 |
73 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/S/ISelect.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
35 |
36 |
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/S/IStamp.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
53 |
![]()
61 |
71 |
74 |
75 |
76 |
77 |
78 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/S/IStampCover.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
56 |
![]()
64 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
84 |
85 |
--------------------------------------------------------------------------------
/src/components/S/IString.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
35 |
36 |
37 |
38 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/S/IText.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/SpaceAvatar.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/SpaceCover.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/SpaceItem.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
24 |
29 |
30 |
31 |
36 |
![]()
40 |
41 |
51 |
52 |
53 |
54 |
55 |
56 | proposals ·
57 | votes
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/components/Stamp.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/StrategyButton.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Transaction.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/Ui/Alert.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/Ui/Button.story.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/Ui/Button.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
28 |
29 |
30 |
42 |
--------------------------------------------------------------------------------
/src/components/Ui/Counter.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Ui/Dropdown.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Ui/DropdownItem.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/Ui/Editable.vue:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
79 |
80 |
81 |
89 |
90 |
91 |
99 |
102 |
105 |
106 |
107 |
108 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/src/components/Ui/Loading.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
34 |
35 |
36 |
37 |
38 |
77 |
--------------------------------------------------------------------------------
/src/components/Ui/Modal.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
112 |
--------------------------------------------------------------------------------
/src/components/Ui/Select.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
29 |
49 |
50 |
51 |
52 |
53 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/components/Ui/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
53 |
54 |
55 |
65 | {{ title }}
66 |
67 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/components/Upload.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
58 |
--------------------------------------------------------------------------------
/src/components/Vote.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
34 | You have already voted for this proposal
35 |
36 |
37 |
38 | You have already voted for this proposal
39 |
40 |
41 | Voting for this proposal hasn't started yet. Voting will start {{ _t(start) }}.
42 |
43 |
44 |
45 | Proposal voting window has ended
46 |
47 |
48 | This proposal has been cancelled
49 |
50 |
51 | Voting for this proposal is not supported
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/VotingPowerIndicator.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
44 |
45 |
54 |
55 | {{ formattedVotingPower }}
56 |
57 |
58 |
59 |
60 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/composables/useAccount.ts:
--------------------------------------------------------------------------------
1 | import { enabledNetworks, getNetwork } from '@/networks';
2 | import type { Vote } from '@/types';
3 |
4 | const votes: Ref> = ref({});
5 |
6 | export function useAccount() {
7 | const { web3 } = useWeb3();
8 |
9 | async function loadVotes() {
10 | const account = web3.value.account;
11 |
12 | const allNetworkVotes = await Promise.all(
13 | enabledNetworks.map(networkId => {
14 | const network = getNetwork(networkId);
15 | return network.api.loadUserVotes(account);
16 | })
17 | );
18 |
19 | votes.value = allNetworkVotes.reduce((acc, b) => ({ ...acc, ...b }));
20 | }
21 |
22 | return { account: web3.value.account, loadVotes, votes };
23 | }
24 |
--------------------------------------------------------------------------------
/src/composables/useApp.ts:
--------------------------------------------------------------------------------
1 | import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
2 |
3 | const state = reactive({
4 | init: false,
5 | loading: false
6 | });
7 |
8 | const { login } = useWeb3();
9 |
10 | export function useApp() {
11 | async function init() {
12 | const auth = getInstance();
13 | state.loading = true;
14 |
15 | // Auto connect with gnosis-connector when inside gnosis-safe iframe
16 | if (window?.parent === window)
17 | auth.getConnector().then(connector => {
18 | if (connector) login(connector);
19 | });
20 | else login('gnosis');
21 |
22 | state.init = true;
23 | state.loading = false;
24 | }
25 |
26 | return {
27 | init,
28 | app: computed(() => state)
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/src/composables/useApps.ts:
--------------------------------------------------------------------------------
1 | // URL: https://docs.google.com/spreadsheets/d/1R1qmDuKTp8WYiy-QWG0WQpu-pfoi-4TTUQKz1XdFZ1o
2 | const APPS_SHEET_ID =
3 | '2PACX-1vSyMqd0Ql198UtPMWO1RQmnzx-rfggEIT3Yieg8mOSf8tyNksUSLKXMpBkO1DLC8yoLqx0stynSk1Us';
4 | const APPS_SHEET_GID = '0';
5 |
6 | async function getSpreadsheet(id: string, gid: string = '0'): Promise {
7 | const res = await fetch(
8 | `https://docs.google.com/spreadsheets/d/e/${id}/pub?output=csv&gid=${gid}&cb=${Math.random()}}`
9 | );
10 | const text = await res.text();
11 |
12 | return csvToJson(text);
13 | }
14 |
15 | function csvToJson(csv: string): any[] {
16 | const [header, ...lines] = csv
17 | .split('\n')
18 | .map(line =>
19 | line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(field => field.trim().replace(/^"|"$/g, ''))
20 | );
21 |
22 | return lines
23 | .filter(line => line.length > 1)
24 | .map(line => Object.fromEntries(header.map((key, i) => [key, line[i] || ''])));
25 | }
26 |
27 | const apps: Ref = ref([]);
28 | const categories: Ref = ref([]);
29 | const loading: Ref = ref(false);
30 | const loaded: Ref = ref(false);
31 |
32 | export function useApps() {
33 | async function load() {
34 | if (loading.value || loaded.value) return;
35 |
36 | loading.value = true;
37 |
38 | apps.value = await getSpreadsheet(APPS_SHEET_ID, APPS_SHEET_GID);
39 | categories.value = [...new Set(apps.value.map(({ category }) => category))];
40 |
41 | loading.value = false;
42 | loaded.value = true;
43 | }
44 |
45 | function get(id: string) {
46 | return apps.value.find(app => app.id === id) || {};
47 | }
48 |
49 | function search(q: string) {
50 | return apps.value.filter(app => {
51 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
52 | const { overview, ...appWithoutOverview } = app;
53 | return JSON.stringify(appWithoutOverview).toLowerCase().includes(q.toLowerCase());
54 | });
55 | }
56 |
57 | return {
58 | apps,
59 | categories,
60 | loading,
61 | loaded,
62 | load,
63 | get,
64 | search
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/composables/useBalances.ts:
--------------------------------------------------------------------------------
1 | import { formatUnits } from '@ethersproject/units';
2 | import { getBalances, GetBalancesResponse } from '@/helpers/alchemy';
3 | import { METADATA } from '@/networks/evm';
4 | import {
5 | ETH_CONTRACT,
6 | COINGECKO_ASSET_PLATFORMS,
7 | COINGECKO_BASE_ASSETS
8 | } from '@/helpers/constants';
9 |
10 | const COINGECKO_API_URL = 'https://api.coingecko.com/api/v3/simple';
11 | const COINGECKO_PARAMS = '&vs_currencies=usd&include_24hr_change=true';
12 |
13 | export const METADATA_BY_CHAIN_ID = new Map(
14 | Object.entries(METADATA).map(([, metadata]) => [metadata.chainId, metadata])
15 | );
16 |
17 | export function useBalances() {
18 | const assets: Ref = ref([]);
19 | const loading = ref(true);
20 | const loaded = ref(false);
21 |
22 | async function callCoinGecko(apiUrl: string) {
23 | const res = await fetch(apiUrl);
24 | return res.json();
25 | }
26 |
27 | async function getCoins(assetPlatform: string, baseToken: string, contractAddresses: string[]) {
28 | const [baseTokenData, tokenData] = await Promise.all([
29 | callCoinGecko(`${COINGECKO_API_URL}/price?ids=${baseToken}${COINGECKO_PARAMS}`),
30 | callCoinGecko(
31 | `${COINGECKO_API_URL}/token_price/${assetPlatform}?contract_addresses=${contractAddresses.join(
32 | ','
33 | )}${COINGECKO_PARAMS}`
34 | )
35 | ]);
36 |
37 | return {
38 | [ETH_CONTRACT]: baseTokenData[baseToken],
39 | ...tokenData
40 | };
41 | }
42 |
43 | async function loadBalances(address: string, networkId: number) {
44 | const metadata = METADATA_BY_CHAIN_ID.get(networkId);
45 | const baseToken = metadata?.ticker
46 | ? { name: metadata.name, symbol: metadata.ticker }
47 | : { name: 'Ether', symbol: 'ETH' };
48 |
49 | const data = await getBalances(address, networkId, baseToken);
50 | const tokensWithBalance = data.filter(
51 | asset => formatUnits(asset.tokenBalance, asset.decimals) !== '0.0'
52 | );
53 |
54 | const coingeckoAssetPlatform = COINGECKO_ASSET_PLATFORMS[networkId];
55 | const coingeckoBaseAsset = COINGECKO_BASE_ASSETS[networkId];
56 |
57 | const coins =
58 | coingeckoBaseAsset && coingeckoAssetPlatform
59 | ? await getCoins(
60 | coingeckoAssetPlatform,
61 | coingeckoBaseAsset,
62 | tokensWithBalance
63 | .filter(asset => asset.contractAddress !== ETH_CONTRACT)
64 | .map(token => token.contractAddress)
65 | )
66 | : [];
67 |
68 | assets.value = tokensWithBalance.map(asset => {
69 | if (!coins[asset.contractAddress]) return asset;
70 |
71 | const price = coins[asset.contractAddress]?.usd || 0;
72 | const change = coins[asset.contractAddress]?.usd_24h_change || 0;
73 | const value = parseFloat(formatUnits(asset.tokenBalance, asset.decimals)) * price;
74 |
75 | return {
76 | ...asset,
77 | price,
78 | change,
79 | value
80 | };
81 | });
82 |
83 | loading.value = false;
84 | loaded.value = true;
85 | }
86 |
87 | const assetsMap = computed(
88 | () => new Map(assets.value.map(asset => [asset.contractAddress, asset]))
89 | );
90 |
91 | return { loading, loaded, assets, assetsMap, loadBalances };
92 | }
93 |
--------------------------------------------------------------------------------
/src/composables/useEditor.ts:
--------------------------------------------------------------------------------
1 | import { lsGet, lsSet, omit } from '@/helpers/utils';
2 | import { Draft, Drafts } from '@/types';
3 |
4 | const proposals = reactive(lsGet('proposals', {}));
5 |
6 | function removeEmpty(proposals: Drafts): Drafts {
7 | return Object.entries(proposals).reduce((acc, [id, proposal]) => {
8 | const { execution, ...rest } = omit(proposal, ['updatedAt']);
9 | const hasFormValues = Object.values(rest).some(val => !!val);
10 |
11 | if (execution.length === 0 && !hasFormValues) {
12 | return acc;
13 | }
14 |
15 | if (proposal.proposalId !== null) {
16 | return acc;
17 | }
18 |
19 | return {
20 | ...acc,
21 | [id]: proposal
22 | };
23 | }, {});
24 | }
25 |
26 | function generateId() {
27 | return (Math.random() + 1).toString(36).substring(7);
28 | }
29 |
30 | function createDraft(
31 | spaceId: string,
32 | payload?: Partial & { proposalId?: number | string },
33 | draftKey?: string
34 | ) {
35 | const id = draftKey || generateId();
36 | const key = `${spaceId}:${id}`;
37 |
38 | proposals[key] = {
39 | title: '',
40 | body: '',
41 | discussion: '',
42 | executionStrategy: null,
43 | execution: [],
44 | updatedAt: Date.now(),
45 | proposalId: null,
46 | ...payload
47 | };
48 | return id;
49 | }
50 |
51 | export function useEditor() {
52 | watch(proposals, () => lsSet('proposals', removeEmpty(proposals)));
53 |
54 | const drafts = computed(() => {
55 | return Object.entries(removeEmpty(proposals))
56 | .map(([k, value]) => {
57 | const [networkId, space, key] = k.split(':');
58 |
59 | return {
60 | id: k,
61 | networkId,
62 | space,
63 | key,
64 | ...value
65 | };
66 | })
67 | .sort((a, b) => b.updatedAt - a.updatedAt);
68 | });
69 |
70 | function removeDraft(key: string) {
71 | delete proposals[key];
72 | }
73 |
74 | return {
75 | proposals,
76 | drafts,
77 | generateId,
78 | createDraft,
79 | removeDraft
80 | };
81 | }
82 |
--------------------------------------------------------------------------------
/src/composables/useFavicon.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_FAVICON = '/favicon-temp.svg';
2 |
3 | export function useFavicon(favicon?: string) {
4 | const setFavicon = (newFavicon: string | null) => {
5 | if (!newFavicon) return setFavicon(DEFAULT_FAVICON);
6 |
7 | document.head
8 | .querySelectorAll('link[rel="icon"]')
9 | .forEach(el => (el.href = newFavicon));
10 | };
11 |
12 | onBeforeUnmount(() => {
13 | setFavicon(DEFAULT_FAVICON);
14 | });
15 |
16 | if (favicon) {
17 | setFavicon(favicon);
18 | }
19 |
20 | return {
21 | setFavicon
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/composables/useMixpanel.ts:
--------------------------------------------------------------------------------
1 | import mixpanel from 'mixpanel-browser';
2 |
3 | export function useMixpanel() {
4 | mixpanel.init(import.meta.env.VITE_MIXPANEL_TOKEN, { ip: false });
5 |
6 | return { mixpanel };
7 | }
8 |
--------------------------------------------------------------------------------
/src/composables/useModal.ts:
--------------------------------------------------------------------------------
1 | const modalOpen = ref(false);
2 | const modalAccountOpen = ref(false);
3 |
4 | export function useModal() {
5 | return { modalOpen, modalAccountOpen };
6 | }
7 |
--------------------------------------------------------------------------------
/src/composables/useNfts.ts:
--------------------------------------------------------------------------------
1 | const SUPPORTED_ABIS = ['ERC721', 'ERC1155'];
2 |
3 | export function useNfts() {
4 | const nfts: Ref = ref([]);
5 | const loading = ref(true);
6 | const loaded = ref(false);
7 |
8 | async function loadNfts(address) {
9 | loading.value = true;
10 |
11 | const url = `https://testnets-api.opensea.io/api/v1/assets?owner=${address}&order_direction=desc&offset=0&limit=20&include_orders=false`;
12 | const res = await fetch(url);
13 | const { assets } = await res.json();
14 |
15 | nfts.value = assets
16 | .filter(asset => SUPPORTED_ABIS.includes(asset.asset_contract?.schema_name))
17 | .map(asset => {
18 | const tokenId = asset.token_id;
19 | const title = asset.name ?? 'Untitled';
20 | const displayTitle = title.match(/(#[0-9]+)$/) || !tokenId ? title : `${title} #${tokenId}`;
21 |
22 | return {
23 | ...asset,
24 | type: asset.asset_contract.schema_name.toLowerCase(),
25 | tokenId,
26 | title,
27 | displayTitle,
28 | image: asset.image_url,
29 | collectionName: asset.collection.name,
30 | contractAddress: asset.asset_contract.address
31 | };
32 | });
33 | loading.value = false;
34 | loaded.value = true;
35 | }
36 |
37 | const nftsMap = computed(() => new Map(nfts.value.map(asset => [asset.id, asset])));
38 |
39 | return { loading, loaded, nfts, nftsMap, loadNfts };
40 | }
41 |
--------------------------------------------------------------------------------
/src/composables/useResolve.ts:
--------------------------------------------------------------------------------
1 | import { resolver } from '@/helpers/resolver';
2 | import { NetworkID } from '@/types';
3 |
4 | export function useResolve(id: Ref) {
5 | const resolved = ref(false);
6 | const networkId: Ref = ref(null);
7 | const address: Ref = ref(null);
8 |
9 | watch(
10 | id,
11 | async id => {
12 | if (!id) return;
13 |
14 | resolved.value = false;
15 |
16 | const resolvedName = await resolver.resolveName(id);
17 | if (resolvedName) {
18 | networkId.value = resolvedName.networkId;
19 | address.value = resolvedName.address;
20 | resolved.value = true;
21 | }
22 | },
23 | { immediate: true }
24 | );
25 |
26 | return {
27 | resolved,
28 | networkId,
29 | address
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/composables/useRouteParser.ts:
--------------------------------------------------------------------------------
1 | import { NetworkID } from '@/types';
2 |
3 | export function useRouteParser(paramName: string) {
4 | const route = useRoute();
5 |
6 | const param = computed(() => route.params[paramName] as string);
7 | const networkId = computed(() => (param.value ? (param.value.split(':')[0] as NetworkID) : null));
8 | const address = computed(() => (param.value ? param.value.split(':')[1] : null));
9 |
10 | return {
11 | param,
12 | networkId,
13 | address
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/composables/useScrollMonitor.ts:
--------------------------------------------------------------------------------
1 | import scrollMonitor from 'scrollmonitor';
2 |
3 | export function useScrollMonitor(fn) {
4 | let elementWatcher;
5 |
6 | const endElement = ref(null);
7 |
8 | onMounted(() => {
9 | // @ts-ignore
10 | elementWatcher = scrollMonitor.create(endElement.value);
11 | elementWatcher.enterViewport(() => {
12 | fn();
13 | });
14 | });
15 |
16 | onBeforeUnmount(() => elementWatcher.destroy());
17 |
18 | return { endElement };
19 | }
20 |
--------------------------------------------------------------------------------
/src/composables/useTitle.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_TITLE = 'Snapshot X';
2 |
3 | export function useTitle(title?: string) {
4 | const setTitle = (newTitle: string) => {
5 | document.title = newTitle;
6 | };
7 |
8 | onBeforeUnmount(() => {
9 | document.title = DEFAULT_TITLE;
10 | });
11 |
12 | if (title) {
13 | setTitle(title);
14 | }
15 |
16 | return {
17 | setTitle
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/composables/useTreasury.ts:
--------------------------------------------------------------------------------
1 | import { CHAIN_IDS } from '@/helpers/constants';
2 | import { Space } from '@/types';
3 |
4 | type NullableSpace = Space | undefined | null;
5 |
6 | export function useTreasury(spaceRef: Ref) {
7 | const treasury = computed(() => {
8 | if (!spaceRef.value || !spaceRef.value.wallet) return null;
9 |
10 | const [networkId, wallet] = spaceRef.value.wallet.split(':');
11 | const chainId = CHAIN_IDS[networkId];
12 | if (!chainId || !wallet) return null;
13 |
14 | return {
15 | networkId,
16 | network: chainId,
17 | wallet
18 | };
19 | });
20 |
21 | return { treasury };
22 | }
23 |
--------------------------------------------------------------------------------
/src/composables/useUserSkin.ts:
--------------------------------------------------------------------------------
1 | import { lsGet, lsSet } from '@/helpers/utils';
2 |
3 | const NOT_SET = 'none';
4 | const DARK_MODE = 'dark';
5 | const LIGHT_MODE = 'light';
6 |
7 | const currenSkin = lsGet('skin', NOT_SET);
8 | // const osSkin = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? LIGHT_MODE : DARK_MODE;
9 | const osSkin = DARK_MODE;
10 |
11 | const userSkin = ref(currenSkin === NOT_SET ? osSkin : currenSkin);
12 | const getMode = () => (userSkin.value === LIGHT_MODE ? LIGHT_MODE : DARK_MODE);
13 | const _toggleSkin = skin => {
14 | if (skin === LIGHT_MODE) {
15 | lsSet('skin', DARK_MODE);
16 | userSkin.value = DARK_MODE;
17 | } else {
18 | lsSet('skin', LIGHT_MODE);
19 | userSkin.value = LIGHT_MODE;
20 | }
21 | };
22 |
23 | export function useUserSkin() {
24 | function toggleSkin() {
25 | const currentSkin = lsGet('skin', NOT_SET);
26 | if (currentSkin === NOT_SET) {
27 | _toggleSkin(osSkin);
28 | } else {
29 | _toggleSkin(currentSkin);
30 | }
31 | }
32 |
33 | watch(
34 | userSkin,
35 | () => {
36 | if (userSkin.value === LIGHT_MODE) {
37 | document.documentElement.classList.remove('dark');
38 | } else {
39 | document.documentElement.classList.add('dark');
40 | }
41 | },
42 | { immediate: true }
43 | );
44 |
45 | return {
46 | userSkin,
47 | getMode,
48 | toggleSkin
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/composables/useWalletConnectTransaction.ts:
--------------------------------------------------------------------------------
1 | import { SelectedStrategy, Transaction } from '@/types';
2 |
3 | const spaceKey = ref(null);
4 | const network = ref(null);
5 | const executionStrategy = ref(null);
6 | const transaction = ref(null);
7 |
8 | export function useWalletConnectTransaction() {
9 | function setTransaction(
10 | _spaceKey: string,
11 | _network: number,
12 | _executionStrategy: SelectedStrategy | null,
13 | _tx: Transaction | null
14 | ) {
15 | spaceKey.value = _spaceKey;
16 | network.value = _network;
17 | executionStrategy.value = _executionStrategy;
18 | transaction.value = _tx;
19 | }
20 |
21 | function reset() {
22 | spaceKey.value = null;
23 | network.value = null;
24 | executionStrategy.value = null;
25 | transaction.value = null;
26 | }
27 |
28 | return {
29 | spaceKey,
30 | network,
31 | executionStrategy,
32 | transaction,
33 | setTransaction,
34 | reset
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/helpers/abis.ts:
--------------------------------------------------------------------------------
1 | export const abis = {
2 | erc20: [
3 | 'constructor(string name, string symbol)',
4 | 'event Approval(address indexed owner, address indexed spender, uint256 value)',
5 | 'event Transfer(address indexed from, address indexed to, uint256 value)',
6 | 'function allowance(address owner, address spender) view returns (uint256)',
7 | 'function approve(address spender, uint256 amount) returns (bool)',
8 | 'function balanceOf(address account) view returns (uint256)',
9 | 'function decimals() view returns (uint8)',
10 | 'function decreaseAllowance(address spender, uint256 subtractedValue) returns (bool)',
11 | 'function increaseAllowance(address spender, uint256 addedValue) returns (bool)',
12 | 'function name() view returns (string)',
13 | 'function symbol() view returns (string)',
14 | 'function totalSupply() view returns (uint256)',
15 | 'function transfer(address recipient, uint256 amount) returns (bool)',
16 | 'function transferFrom(address sender, address recipient, uint256 amount) returns (bool)'
17 | ],
18 | erc721: ['function safeTransferFrom(address from, address to, uint256 tokenId)'],
19 | erc1155: [
20 | 'function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)'
21 | ]
22 | };
23 |
--------------------------------------------------------------------------------
/src/helpers/alchemy/types.ts:
--------------------------------------------------------------------------------
1 | export type BalanceData = { contractAddress: string; tokenBalance: string };
2 | export type Metadata = {
3 | decimals: number;
4 | logo: string | null;
5 | name: string;
6 | symbol: string;
7 | };
8 | export type Token = BalanceData &
9 | Metadata & {
10 | price: number;
11 | value: number;
12 | change: number;
13 | };
14 |
15 | export type GetTokenBalancesResponse = {
16 | address: string;
17 | tokenBalances: BalanceData[];
18 | };
19 |
20 | export type GetTokensMetadataResponse = Metadata[];
21 |
22 | export type GetBalancesResponse = Token[];
23 |
--------------------------------------------------------------------------------
/src/helpers/argentx.ts:
--------------------------------------------------------------------------------
1 | const get = () => import(/* webpackChunkName: "argentx" */ '@argent/get-starknet');
2 | import LockConnector from '@snapshot-labs/lock/src/connector';
3 |
4 | export default class Connector extends LockConnector {
5 | async connect() {
6 | let provider;
7 | try {
8 | const argentx = await get();
9 | const starknet = await argentx.connect();
10 |
11 | if (!starknet) {
12 | throw Error('User rejected wallet selection or silent connect found nothing');
13 | }
14 |
15 | // @ts-ignore starknetVersion is expected by ArgentX, v3 won't work with new transactions
16 | await starknet.enable({ showModal: true, starknetVersion: 'v4' });
17 |
18 | if (!starknet.isConnected) {
19 | throw new Error('Connector was not connected');
20 | }
21 |
22 | provider = starknet;
23 | provider.connectorName = 'argentx';
24 | return provider;
25 | } catch (e) {
26 | console.error(e);
27 | return false;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/helpers/auth.ts:
--------------------------------------------------------------------------------
1 | import injected from '@snapshot-labs/lock/connectors/injected';
2 | import walletconnect from '@snapshot-labs/lock/connectors/walletconnect';
3 | import connectors from '@/helpers/connectors';
4 | import walletlink from '@snapshot-labs/lock/connectors/walletlink';
5 | import gnosis from '@snapshot-labs/lock/connectors/gnosis';
6 | import argentx from '@/helpers/argentx';
7 |
8 | const options: any = { connectors: [] };
9 | const lockConnectors = {
10 | injected,
11 | walletconnect,
12 | walletlink,
13 | gnosis,
14 | argentx
15 | };
16 |
17 | Object.entries(connectors).forEach((connector: any) => {
18 | options.connectors.push({
19 | key: connector[0],
20 | connector: lockConnectors[connector[0]],
21 | options: connector[1].options
22 | });
23 | });
24 |
25 | export default options;
26 |
--------------------------------------------------------------------------------
/src/helpers/call.ts:
--------------------------------------------------------------------------------
1 | import { Interface } from '@ethersproject/abi';
2 | import { Contract } from '@ethersproject/contracts';
3 | import networks from '@/helpers/networks.json';
4 |
5 | export async function call(provider, abi: any[], call: any[], options?) {
6 | const contract = new Contract(call[0], abi, provider);
7 | try {
8 | const params = call[2] || [];
9 | return await contract[call[1]](...params, options || {});
10 | } catch (e) {
11 | return Promise.reject(e);
12 | }
13 | }
14 |
15 | export async function multicall(network: string, provider, abi: any[], calls: any[], options?) {
16 | const multicallAbi = [
17 | 'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)'
18 | ];
19 | const multi = new Contract(networks[network].multicall, multicallAbi, provider);
20 | const itf = new Interface(abi);
21 | try {
22 | const max = options?.limit || 500;
23 | const pages = Math.ceil(calls.length / max);
24 | const promises: any = [];
25 | Array.from(Array(pages)).forEach((x, i) => {
26 | const callsInPage = calls.slice(max * i, max * (i + 1));
27 | promises.push(
28 | multi.aggregate(
29 | callsInPage.map(call => [
30 | call[0].toLowerCase(),
31 | itf.encodeFunctionData(call[1], call[2])
32 | ]),
33 | options || {}
34 | )
35 | );
36 | });
37 | let results: any = await Promise.all(promises);
38 | results = results.reduce((prev: any, [, res]: any) => prev.concat(res), []);
39 | return results.map((call, i) => itf.decodeFunctionResult(calls[i][1], call));
40 | } catch (e) {
41 | return Promise.reject(e);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/helpers/connectors.ts:
--------------------------------------------------------------------------------
1 | import { getUrl } from '@/helpers/utils';
2 | import metamaskIcon from '@/assets/connectors/metamask.png';
3 | import walletconnectIcon from '@/assets/connectors/walletconnect.png';
4 | import coinbaseIcon from '@/assets/connectors/coinbase.png';
5 | import gnosisIcon from '@/assets/connectors/gnosis.png';
6 | import starknetIcon from '@/assets/connectors/starknet.png';
7 |
8 | export default {
9 | injected: {
10 | id: 'injected',
11 | name: 'MetaMask',
12 | type: 'injected',
13 | root: 'ethereum',
14 | icon: metamaskIcon
15 | },
16 | walletconnect: {
17 | id: 'walletconnect',
18 | name: 'WalletConnect',
19 | network: '1',
20 | icon: walletconnectIcon,
21 | options: {
22 | projectId: 'e6454bd61aba40b786e866a69bd4c5c6',
23 | chains: [5],
24 | optionalChains: [1, 42161, 137, 11155111],
25 | methods: ['eth_sendTransaction', 'eth_signTypedData_v4'],
26 | showQrModal: true
27 | }
28 | },
29 | walletlink: {
30 | id: 'walletlink',
31 | name: 'Coinbase',
32 | network: '1',
33 | icon: coinbaseIcon,
34 | options: {
35 | appName: 'Snapshot',
36 | darkMode: false,
37 | chainId: 1,
38 | ethJsonrpcUrl: 'https://cloudflare-eth.com'
39 | }
40 | },
41 | gnosis: {
42 | id: 'gnosis',
43 | type: 'gnosis',
44 | name: 'Gnosis Safe',
45 | icon: gnosisIcon
46 | },
47 | argentx: {
48 | id: 'argentx',
49 | name: 'Starknet',
50 | type: 'injected',
51 | root: 'starknet',
52 | icon: starknetIcon
53 | }
54 | };
55 |
56 | export function mapConnectorId(sourceName: string) {
57 | if (sourceName === 'metamask') return 'injected';
58 | if (sourceName === 'coinbase') return 'walletlink';
59 |
60 | return sourceName;
61 | }
62 |
63 | export function getConnectorIconUrl(url) {
64 | if (url.startsWith('ipfs://')) return getUrl(url);
65 |
66 | return url;
67 | }
68 |
--------------------------------------------------------------------------------
/src/helpers/constants.ts:
--------------------------------------------------------------------------------
1 | export const ETH_CONTRACT = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
2 |
3 | export const CHAIN_IDS = {
4 | matic: 137,
5 | arb1: 42161,
6 | eth: 1,
7 | gor: 5,
8 | sep: 11155111,
9 | 'linea-testnet': 59140
10 | };
11 |
12 | export const COINGECKO_ASSET_PLATFORMS = {
13 | 137: 'polygon-pos',
14 | 42161: 'arbitrum-one'
15 | };
16 |
17 | export const COINGECKO_BASE_ASSETS = {
18 | 137: 'matic-network',
19 | 42161: 'ethereum'
20 | };
21 |
22 | export const MAX_SYMBOL_LENGTH = 12;
23 | export const CHOICES = ['For', 'Against', 'Abstain'];
24 |
--------------------------------------------------------------------------------
/src/helpers/ens.ts:
--------------------------------------------------------------------------------
1 | import { namehash } from '@ethersproject/hash';
2 | import { getProvider } from '@/helpers/provider';
3 | import { call } from '@/helpers/call';
4 |
5 | const abi = ['function addr(bytes32 node) view returns (address r)'];
6 |
7 | const ensPublicResolvers = {
8 | 1: '0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41',
9 | 5: '0xd7a4F6473f32aC2Af804B3686AE8F1932bC35750'
10 | };
11 |
12 | export async function resolveName(name: string, chainId: number) {
13 | const resolver = ensPublicResolvers[chainId];
14 | if (!resolver) throw new Error('Unsupported chainId');
15 |
16 | const provider = getProvider(chainId);
17 | const node = namehash(name);
18 |
19 | const address: string = await call(provider, abi, [resolver, 'addr', [node]], {
20 | blockTag: 'latest'
21 | });
22 |
23 | if (address === '0x0000000000000000000000000000000000000000') return null;
24 |
25 | return address;
26 | }
27 |
--------------------------------------------------------------------------------
/src/helpers/etherscan.ts:
--------------------------------------------------------------------------------
1 | export async function getABI(chainId: number, address: string) {
2 | let apiHost;
3 | if (chainId === 1) apiHost = 'https://api.etherscan.io';
4 | else if (chainId === 5) apiHost = 'https://api-goerli.etherscan.io';
5 | else if (chainId === 11155111) apiHost = 'https://api-sepolia.etherscan.io';
6 | else throw new Error('Unsupported chainId');
7 |
8 | const params = new URLSearchParams({
9 | module: 'contract',
10 | action: 'getAbi',
11 | address
12 | });
13 |
14 | const res = await fetch(`${apiHost}/api?${params}`);
15 | const { result } = await res.json();
16 |
17 | return JSON.parse(result);
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/execution.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0x6810e776880C02933D47DB1b9fc05908e5386b96",
4 | "data": "0xa9059cbb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b5e3af16b1880000",
5 | "nonce": 0,
6 | "operation": "0",
7 | "type": "contractInteraction",
8 | "value": "0",
9 | "abi": ["function transfer(address _to, uint256 _value) returns (bool)"]
10 | }
11 | ]
12 |
--------------------------------------------------------------------------------
/src/helpers/mana.ts:
--------------------------------------------------------------------------------
1 | export const MANA_URL = import.meta.env.VITE_MANA_URL || 'http://localhost:3000';
2 |
3 | async function rpcCall(path: string, method: string, params: any) {
4 | const res = await fetch(`${MANA_URL}/${path}`, {
5 | method: 'POST',
6 | headers: {
7 | 'Content-Type': 'application/json'
8 | },
9 | body: JSON.stringify({
10 | jsonrpc: '2.0',
11 | method,
12 | params,
13 | id: null
14 | })
15 | });
16 |
17 | const { error, result } = await res.json();
18 | if (error) throw new Error('RPC call failed');
19 |
20 | return result;
21 | }
22 |
23 | export async function registerTransaction(
24 | chainId: number | string,
25 | params: {
26 | type: string;
27 | hash: string;
28 | payload: any;
29 | }
30 | ) {
31 | return rpcCall(`stark_rpc/${chainId}`, 'registerTransaction', params);
32 | }
33 |
34 | export async function executionCall(
35 | chainId: number,
36 | method: 'execute' | 'executeQueuedProposal',
37 | params: any
38 | ) {
39 | return rpcCall(`eth_rpc/${chainId}`, method, params);
40 | }
41 |
--------------------------------------------------------------------------------
/src/helpers/multicaller.ts:
--------------------------------------------------------------------------------
1 | import { StaticJsonRpcProvider } from '@ethersproject/providers';
2 | import set from 'lodash.set';
3 | import { multicall } from '@/helpers/call';
4 |
5 | export default class Multicaller {
6 | public network: string;
7 | public provider: StaticJsonRpcProvider;
8 | public abi: any[];
9 | public options: any = {};
10 | public calls: any[] = [];
11 | public paths: any[] = [];
12 |
13 | constructor(network: string, provider: StaticJsonRpcProvider, abi: any[], options?) {
14 | this.network = network;
15 | this.provider = provider;
16 | this.abi = abi;
17 | this.options = options || {};
18 | }
19 |
20 | call(path, address, fn, params?): Multicaller {
21 | this.calls.push([address, fn, params]);
22 | this.paths.push(path);
23 | return this;
24 | }
25 |
26 | async execute(from?: any): Promise {
27 | const obj = from || {};
28 | const result = await multicall(this.network, this.provider, this.abi, this.calls, this.options);
29 | result.forEach((r, i) => set(obj, this.paths[i], r.length > 1 ? r : r[0]));
30 | this.calls = [];
31 | this.paths = [];
32 | return obj;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/helpers/networks.json:
--------------------------------------------------------------------------------
1 | {
2 | "1": {
3 | "key": "1",
4 | "chainId": 1,
5 | "multicall": "0xeefba1e63905ef1d7acba5a8513c70307c1ce441",
6 | "explorer": "https://etherscan.io"
7 | },
8 | "137": {
9 | "key": "137",
10 | "chainId": 137,
11 | "multicall": "0xCBca837161be50EfA5925bB9Cc77406468e76751",
12 | "explorer": "https://polygonscan.com"
13 | },
14 | "42161": {
15 | "key": "42161",
16 | "chainId": 42161,
17 | "multicall": "0x7A7443F8c577d537f1d8cD4a629d40a3148Dd7ee",
18 | "explorer": "https://arbiscan.io"
19 | },
20 | "5": {
21 | "key": "5",
22 | "chainId": 5,
23 | "multicall": "0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e",
24 | "explorer": "https://goerli.etherscan.io"
25 | },
26 | "11155111": {
27 | "key": "11155111",
28 | "chainId": 11155111,
29 | "multicall": "0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e",
30 | "explorer": "https://sepolia.etherscan.io"
31 | },
32 | "59140": {
33 | "key": "59140",
34 | "chainId": 59140,
35 | "multicall": "0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e",
36 | "explorer": "https://goerli.lineascan.build"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/helpers/pin.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'ipfs-http-client';
2 | import { pin } from '@snapshot-labs/pineapple';
3 |
4 | const client = create({ url: 'https://api.thegraph.com/ipfs/api/v0' });
5 |
6 | export async function pinGraph(payload: any) {
7 | const res = await client.add(JSON.stringify(payload), { pin: true });
8 |
9 | return {
10 | provider: 'graph',
11 | cid: res.cid.toV0().toString()
12 | };
13 | }
14 |
15 | export async function pinPineapple(payload: any) {
16 | const pinned = await pin(payload);
17 | if (!pinned) throw new Error('Failed to pin');
18 |
19 | return {
20 | provider: pinned.provider,
21 | cid: pinned.cid
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/helpers/provider.ts:
--------------------------------------------------------------------------------
1 | import { StaticJsonRpcProvider } from '@ethersproject/providers';
2 |
3 | const providers: Record = {};
4 |
5 | export function getProvider(networkId: number): StaticJsonRpcProvider {
6 | const url = `https://rpc.snapshotx.xyz/${networkId}`;
7 |
8 | let provider = providers[networkId];
9 |
10 | if (!provider) {
11 | provider = new StaticJsonRpcProvider({ url, timeout: 25000 }, networkId);
12 | providers[networkId] = provider;
13 | }
14 |
15 | return provider;
16 | }
17 |
--------------------------------------------------------------------------------
/src/helpers/resolver.ts:
--------------------------------------------------------------------------------
1 | import { memoize } from '@/helpers/utils';
2 | import { resolveName as resolveEnsName } from '@/helpers/ens';
3 | import { NetworkID } from '@/types';
4 | import { offchainNetworks } from '@/networks';
5 |
6 | const ENS_CHAIN_ID = 5;
7 | const ENS_NETWORK_ID = 'gor';
8 |
9 | type ResolvedName = {
10 | networkId: NetworkID;
11 | address: string;
12 | };
13 |
14 | function createResolver() {
15 | const cache = new Map();
16 |
17 | function resolveStatic(id: string): ResolvedName | null {
18 | const parts = id.split(':');
19 |
20 | return {
21 | networkId: parts[0] as NetworkID,
22 | address: parts[1]
23 | };
24 | }
25 |
26 | async function resolveEns(id: string): Promise {
27 | const resolvedAddress = await resolveEnsName(id, ENS_CHAIN_ID);
28 |
29 | if (!resolvedAddress) {
30 | return null;
31 | }
32 |
33 | return {
34 | networkId: ENS_NETWORK_ID,
35 | address: resolvedAddress.toLocaleLowerCase()
36 | };
37 | }
38 |
39 | async function resolveName(id: string) {
40 | if (cache.has(id)) {
41 | return cache.get(id);
42 | }
43 |
44 | const shouldUseEns =
45 | id.endsWith('.eth') && !offchainNetworks.includes(id.split(':')[0] as NetworkID);
46 |
47 | const resolved = shouldUseEns ? await resolveEns(id) : resolveStatic(id);
48 | if (resolved) {
49 | cache.set(id, resolved);
50 | }
51 |
52 | return resolved;
53 | }
54 |
55 | return {
56 | resolveName: memoize(resolveName)
57 | };
58 | }
59 |
60 | export const resolver = createResolver();
61 |
--------------------------------------------------------------------------------
/src/helpers/stamp.ts:
--------------------------------------------------------------------------------
1 | export async function getNames(addresses: string[]): Promise> {
2 | try {
3 | const res = await fetch('https://stamp.fyi', {
4 | method: 'POST',
5 | headers: {
6 | 'Content-Type': 'application/json'
7 | },
8 | body: JSON.stringify({ method: 'lookup_addresses', params: addresses })
9 | });
10 | const data = (await res.json()).result;
11 |
12 | const dataToLc = Object.fromEntries(Object.entries(data).map(([k, v]) => [k.toLowerCase(), v]));
13 | const entries: any = addresses
14 | .map(address => [address, dataToLc[address.toLowerCase()] || null])
15 | .filter(([, name]) => name);
16 |
17 | return Object.fromEntries(entries);
18 | } catch (e) {
19 | console.error('Failed to resolve names', e);
20 | return {};
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/helpers/tenderly.ts:
--------------------------------------------------------------------------------
1 | import { Transaction } from '@/types';
2 |
3 | const TENDERLY_ACCESS_KEY = import.meta.env.VITE_TENDERLY_ACCESS_KEY;
4 |
5 | export async function simulate(chainId: number, from: string, txs: Transaction[]) {
6 | const url = 'https://api.tenderly.co/api/v1/account/me/project/project/simulate-batch';
7 |
8 | const init = {
9 | method: 'POST',
10 | headers: {
11 | Accept: 'application/json',
12 | 'Content-Type': 'application/json',
13 | 'X-Access-Key': TENDERLY_ACCESS_KEY
14 | },
15 | body: JSON.stringify({
16 | simulations: txs.map(tx => ({
17 | network_id: chainId,
18 | from,
19 | to: tx.to,
20 | input: tx.data,
21 | gas_price: '0',
22 | value: tx.value
23 | }))
24 | })
25 | };
26 |
27 | try {
28 | const res = await fetch(url, init);
29 | const data = await res.json();
30 |
31 | return !data.simulation_results.find(result => result.transaction.status === false);
32 | } catch (e) {
33 | return false;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/helpers/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 |
3 | import { createErc1155Metadata } from './utils';
4 |
5 | describe('utils', () => {
6 | describe('createErc1155Metadata', () => {
7 | it('should create metadata', () => {
8 | const metadata = createErc1155Metadata({
9 | name: 'Test',
10 | avatar: '',
11 | cover: '',
12 | description: 'Test description',
13 | externalUrl: 'https://test.com',
14 | github: 'snapshot-labs',
15 | twitter: 'SnapshotLabs',
16 | discord: 'snapshot',
17 | votingPowerSymbol: 'VOTE',
18 | walletNetwork: 'gor',
19 | walletAddress: '0x000000000000000000000000000000000000dead',
20 | delegations: [
21 | {
22 | name: 'sample',
23 | apiType: 'governor-subgraph',
24 | apiUrl: 'https://thegraph.com/hosted-service/subgraph/arr00/uniswap-governance-v2',
25 | contractNetwork: 'gor',
26 | contractAddress: '0x000000000000000000000000000000000000dead'
27 | }
28 | ]
29 | });
30 |
31 | expect(metadata).toEqual({
32 | name: 'Test',
33 | avatar: '',
34 | description: 'Test description',
35 | external_url: 'https://test.com',
36 | properties: {
37 | voting_power_symbol: 'VOTE',
38 | cover: '',
39 | github: 'snapshot-labs',
40 | twitter: 'SnapshotLabs',
41 | discord: 'snapshot',
42 | wallets: ['gor:0x000000000000000000000000000000000000dead'],
43 | delegations: [
44 | {
45 | name: 'sample',
46 | api_type: 'governor-subgraph',
47 | api_url: 'https://thegraph.com/hosted-service/subgraph/arr00/uniswap-governance-v2',
48 | contract: 'gor:0x000000000000000000000000000000000000dead'
49 | }
50 | ]
51 | }
52 | });
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/histoire-setup.ts:
--------------------------------------------------------------------------------
1 | import './style.scss';
2 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createPinia } from 'pinia';
2 | import { LockPlugin } from '@snapshot-labs/lock/plugins/vue3';
3 | import options from '@/helpers/auth';
4 | import App from '@/App.vue';
5 | import router from '@/router';
6 | import '@/helpers/auth';
7 | import '@/style.scss';
8 |
9 | const knownHosts = ['app.safe.global', 'pilot.gnosisguild.org'];
10 | const parentUrl =
11 | window.location != window.parent.location
12 | ? document.referrer ||
13 | document.location.ancestorOrigins[document.location.ancestorOrigins.length - 1]
14 | : document.location.href;
15 | const parentHost = new URL(parentUrl).host;
16 | if (window !== window.parent && !knownHosts.includes(parentHost)) {
17 | document.documentElement.style.display = 'none';
18 | throw new Error(`Unknown host: ${parentHost}`);
19 | }
20 |
21 | const pinia = createPinia();
22 | const app = createApp({ render: () => h(App) })
23 | .use(router)
24 | .use(LockPlugin, options);
25 |
26 | app.use(pinia);
27 |
28 | app.mount('#app');
29 |
30 | export default app;
31 |
--------------------------------------------------------------------------------
/src/networks/common/constants.ts:
--------------------------------------------------------------------------------
1 | import { Connector } from '@/networks/types';
2 |
3 | export const EVM_CONNECTORS: Connector[] = ['injected', 'walletconnect', 'walletlink', 'gnosis'];
4 |
5 | export const STARKNET_CONNECTORS: Connector[] = ['argentx'];
6 |
--------------------------------------------------------------------------------
/src/networks/common/graphqlApi/types.ts:
--------------------------------------------------------------------------------
1 | export type ApiStrategyParsedMetadata = {
2 | index: number;
3 | data: {
4 | name: string;
5 | description: string;
6 | decimals: number;
7 | symbol: string;
8 | token: string | null;
9 | payload: string | null;
10 | };
11 | };
12 |
13 | export type ApiSpace = {
14 | id: string;
15 | metadata: {
16 | name: string;
17 | avatar: string;
18 | cover: string;
19 | about?: string;
20 | external_url: string;
21 | twitter: string;
22 | github: string;
23 | discord: string;
24 | voting_power_symbol: string;
25 | wallet: string;
26 | executors: string[];
27 | executors_types: string[];
28 | delegations: string[];
29 | };
30 | controller: string;
31 | voting_delay: number;
32 | min_voting_period: number;
33 | max_voting_period: number;
34 | proposal_threshold: string;
35 | validation_strategy: string;
36 | validation_strategy_params: string;
37 | voting_power_validation_strategy_strategies: string[];
38 | voting_power_validation_strategy_strategies_params: string[];
39 | voting_power_validation_strategies_parsed_metadata: ApiStrategyParsedMetadata[];
40 | strategies_indicies: number[];
41 | strategies: string[];
42 | strategies_params: any[];
43 | strategies_parsed_metadata: ApiStrategyParsedMetadata[];
44 | authenticators: string[];
45 | proposal_count: number;
46 | vote_count: number;
47 | created: number;
48 | };
49 |
50 | export type ApiProposal = {
51 | id: string;
52 | proposal_id: number;
53 | metadata: {
54 | id: string;
55 | title: string;
56 | body: string;
57 | discussion: string;
58 | execution: string;
59 | };
60 | space: {
61 | id: string;
62 | controller: string;
63 | metadata: {
64 | name: string;
65 | avatar: string;
66 | voting_power_symbol: string;
67 | executors: string[];
68 | executors_types: string[];
69 | };
70 | authenticators: string[];
71 | strategies_parsed_metadata: ApiStrategyParsedMetadata[];
72 | };
73 | author: {
74 | id: string;
75 | };
76 | quorum: number;
77 | execution_hash: string;
78 | start: number;
79 | min_end: number;
80 | max_end: number;
81 | snapshot: number;
82 | // TODO: those are actually numbers, we need to adjust it across the app at some point
83 | scores_1: number;
84 | scores_2: number;
85 | scores_3: number;
86 | scores_total: number;
87 | execution_time: number;
88 | execution_strategy: string;
89 | execution_strategy_type: string;
90 | timelock_veto_guardian: string | null;
91 | strategies_indicies: number[];
92 | strategies: string[];
93 | strategies_params: any[];
94 | created: number;
95 | edited: number | null;
96 | tx: string;
97 | execution_tx: string | null;
98 | veto_tx: string | null;
99 | vote_count: number;
100 | executed: boolean;
101 | vetoed: boolean;
102 | completed: boolean;
103 | cancelled: boolean;
104 | };
105 |
--------------------------------------------------------------------------------
/src/networks/evm/helpers.ts:
--------------------------------------------------------------------------------
1 | export async function executionCall(
2 | baseUrl: string,
3 | chainId: number,
4 | method: 'execute' | 'executeQueuedProposal',
5 | params: any
6 | ) {
7 | const res = await fetch(`${baseUrl}/eth_rpc/${chainId}`, {
8 | method: 'POST',
9 | headers: {
10 | 'Content-Type': 'application/json'
11 | },
12 | body: JSON.stringify({
13 | jsonrpc: '2.0',
14 | method: method,
15 | params,
16 | id: null
17 | })
18 | });
19 |
20 | const { error, result } = await res.json();
21 | if (error) throw new Error('Finalization failed');
22 |
23 | return result;
24 | }
25 |
--------------------------------------------------------------------------------
/src/networks/index.ts:
--------------------------------------------------------------------------------
1 | import { createStarknetNetwork } from './starknet';
2 | import { createEvmNetwork } from './evm';
3 | import { createOffchainNetwork } from './offchain';
4 | import { NetworkID } from '@/types';
5 | import { ReadWriteNetwork } from './types';
6 |
7 | const snapshotNetwork = createOffchainNetwork('s');
8 | const snapshotTestnetNetwork = createOffchainNetwork('s-tn');
9 | const starknetNetwork = createStarknetNetwork('sn');
10 | const starknetTestnetNetwork = createStarknetNetwork('sn-tn');
11 | const starknetSepoliaNetwork = createStarknetNetwork('sn-sep');
12 | const polygonNetwork = createEvmNetwork('matic');
13 | const arbitrumNetwork = createEvmNetwork('arb1');
14 | const ethereumNetwork = createEvmNetwork('eth');
15 | const goerliNetwork = createEvmNetwork('gor');
16 | const sepoliaNetwork = createEvmNetwork('sep');
17 | const lineaTestnetNetwork = createEvmNetwork('linea-testnet');
18 |
19 | export const enabledNetworks: NetworkID[] = import.meta.env.VITE_ENABLED_NETWORKS
20 | ? (import.meta.env.VITE_ENABLED_NETWORKS.split(',') as NetworkID[])
21 | : ['s', 's-tn', 'eth', 'matic', 'arb1', 'gor', 'sep', 'sn', 'sn-sep'];
22 |
23 | export const evmNetworks: NetworkID[] = ['eth', 'matic', 'arb1', 'gor', 'sep', 'linea-testnet'];
24 | export const offchainNetworks: NetworkID[] = ['s', 's-tn'];
25 |
26 | export const getNetwork = (id: NetworkID) => {
27 | if (!enabledNetworks.includes(id)) throw new Error(`Network ${id} is not enabled`);
28 |
29 | if (id === 's') return snapshotNetwork;
30 | if (id === 's-tn') return snapshotTestnetNetwork;
31 | if (id === 'matic') return polygonNetwork;
32 | if (id === 'arb1') return arbitrumNetwork;
33 | if (id === 'eth') return ethereumNetwork;
34 | if (id === 'gor') return goerliNetwork;
35 | if (id === 'sep') return sepoliaNetwork;
36 | if (id === 'linea-testnet') return lineaTestnetNetwork;
37 | if (id === 'sn') return starknetNetwork;
38 | if (id === 'sn-tn') return starknetTestnetNetwork;
39 | if (id === 'sn-sep') return starknetSepoliaNetwork;
40 |
41 | throw new Error(`Unknown network ${id}`);
42 | };
43 |
44 | export const getReadWriteNetwork = (id: NetworkID): ReadWriteNetwork => {
45 | const network = getNetwork(id);
46 | if (network.readOnly) throw new Error(`Network ${id} is read-only`);
47 |
48 | return network;
49 | };
50 |
51 | /**
52 | * supportsNullCurrent return true if the network supports null current to be used for computing current voting power
53 | * @param networkId Network ID
54 | * @returns boolean true if the network supports null current
55 | */
56 | export const supportsNullCurrent = (networkID: NetworkID) => {
57 | return !evmNetworks.includes(networkID);
58 | };
59 |
--------------------------------------------------------------------------------
/src/networks/offchain/api/queries.ts:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | const SPACE_FRAGMENT = gql`
4 | fragment spaceFragment on Space {
5 | id
6 | admins
7 | name
8 | about
9 | website
10 | twitter
11 | github
12 | symbol
13 | treasuries {
14 | network
15 | address
16 | }
17 | delegationPortal {
18 | delegationType
19 | delegationContract
20 | delegationApi
21 | }
22 | voting {
23 | delay
24 | period
25 | quorum
26 | }
27 | proposalsCount
28 | votesCount
29 | }
30 | `;
31 |
32 | const PROPOSAL_FRAGMENT = gql`
33 | fragment proposalFragment on Proposal {
34 | id
35 | ipfs
36 | space {
37 | id
38 | name
39 | admins
40 | symbol
41 | }
42 | type
43 | title
44 | body
45 | discussion
46 | author
47 | quorum
48 | start
49 | end
50 | snapshot
51 | choices
52 | scores
53 | scores_total
54 | state
55 | created
56 | updated
57 | votes
58 | }
59 | `;
60 |
61 | export const PROPOSAL_QUERY = gql`
62 | query ($id: String!) {
63 | proposal(id: $id) {
64 | ...proposalFragment
65 | }
66 | }
67 | ${PROPOSAL_FRAGMENT}
68 | `;
69 |
70 | export const PROPOSALS_QUERY = gql`
71 | query ($first: Int!, $skip: Int!, $where: ProposalWhere) {
72 | proposals(first: $first, skip: $skip, where: $where, orderBy: "created", orderDirection: desc) {
73 | ...proposalFragment
74 | }
75 | }
76 | ${PROPOSAL_FRAGMENT}
77 | `;
78 |
79 | export const SPACES_RANKING_QUERY = gql`
80 | query ($first: Int, $skip: Int, $where: SpaceWhere) {
81 | spaces(first: $first, skip: $skip, where: $where) {
82 | ...spaceFragment
83 | }
84 | }
85 | ${SPACE_FRAGMENT}
86 | `;
87 |
88 | export const SPACE_QUERY = gql`
89 | query ($id: String!) {
90 | space(id: $id) {
91 | ...spaceFragment
92 | }
93 | }
94 | ${SPACE_FRAGMENT}
95 | `;
96 |
97 | export const VOTES_QUERY = gql`
98 | query (
99 | $first: Int!
100 | $skip: Int!
101 | $orderBy: String!
102 | $orderDirection: OrderDirection!
103 | $where: VoteWhere
104 | ) {
105 | votes(
106 | first: $first
107 | skip: $skip
108 | where: $where
109 | orderBy: $orderBy
110 | orderDirection: $orderDirection
111 | ) {
112 | id
113 | voter
114 | space {
115 | id
116 | }
117 | proposal {
118 | id
119 | }
120 | ipfs
121 | choice
122 | vp
123 | created
124 | }
125 | }
126 | `;
127 |
--------------------------------------------------------------------------------
/src/networks/offchain/api/types.ts:
--------------------------------------------------------------------------------
1 | export type ApiSpace = {
2 | id: string;
3 | admins: string[];
4 | name: string;
5 | about: string;
6 | website: string;
7 | twitter: string;
8 | github: string;
9 | symbol: string;
10 | treasuries: [
11 | {
12 | network: string;
13 | address: string;
14 | }
15 | ];
16 | delegationPortal?: {
17 | delegationType: string;
18 | delegationContract: string;
19 | delegationApi: string;
20 | };
21 | voting: {
22 | delay: number | null;
23 | period: number | null;
24 | quorum: number | null;
25 | };
26 | proposalsCount: number;
27 | votesCount: number;
28 | };
29 |
30 | export type ApiProposal = {
31 | id: string;
32 | ipfs: string;
33 | space: {
34 | id: string;
35 | name: string;
36 | admins: string[];
37 | symbol: string;
38 | };
39 | type: 'basic' | 'single-choice' | string;
40 | title: string;
41 | body: string;
42 | discussion: string;
43 | author: string;
44 | quorum: number;
45 | start: number;
46 | end: number;
47 | snapshot: number;
48 | choices: string[];
49 | scores: number[];
50 | scores_total: number;
51 | state: 'active' | 'pending' | 'closed';
52 | created: number;
53 | updated: number | null;
54 | votes: number;
55 | };
56 |
57 | export type ApiVote = {
58 | id: string;
59 | voter: string;
60 | ipfs: string;
61 | space: {
62 | id: string;
63 | };
64 | proposal: {
65 | id: string;
66 | };
67 | choice: number | number[];
68 | vp: number;
69 | created: number;
70 | };
71 |
--------------------------------------------------------------------------------
/src/networks/offchain/constants.ts:
--------------------------------------------------------------------------------
1 | export const SUPPORTED_AUTHENTICATORS = {};
2 | export const CONTRACT_SUPPORTED_AUTHENTICATORS = {};
3 | export const SUPPORTED_STRATEGIES = {};
4 | export const SUPPORTED_EXECUTORS = {};
5 | export const RELAYER_AUTHENTICATORS = {};
6 | export const AUTHS = {};
7 | export const PROPOSAL_VALIDATIONS = {};
8 | export const STRATEGIES = {};
9 | export const EXECUTORS = {};
10 | export const EDITOR_AUTHENTICATORS = [];
11 | export const EDITOR_PROPOSAL_VALIDATIONS = [];
12 | export const EDITOR_VOTING_STRATEGIES = [];
13 | export const EDITOR_PROPOSAL_VALIDATION_VOTING_STRATEGIES = [];
14 | export const EDITOR_EXECUTION_STRATEGIES = [];
15 |
--------------------------------------------------------------------------------
/src/networks/offchain/index.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from './api';
2 | import * as constants from './constants';
3 | import { pinPineapple } from '@/helpers/pin';
4 | import { Network } from '@/networks/types';
5 | import { NetworkID } from '@/types';
6 |
7 | const HUB_URLS: Partial> = {
8 | s: 'https://hub.snapshot.org/graphql',
9 | 's-tn': 'https://testnet.hub.snapshot.org/graphql'
10 | };
11 |
12 | export function createOffchainNetwork(networkId: NetworkID): Network {
13 | const l1ChainId = 1;
14 |
15 | const hubUrl = HUB_URLS[networkId];
16 | if (!hubUrl) throw new Error(`Unknown network ${networkId}`);
17 |
18 | const api = createApi(hubUrl, networkId);
19 |
20 | const helpers = {
21 | pin: pinPineapple,
22 | waitForTransaction: () => {
23 | throw new Error('Not implemented');
24 | },
25 | waitForSpace: () => {
26 | throw new Error('Not implemented');
27 | },
28 | getExplorerUrl: (id: string, type: 'transaction' | 'address' | 'contract' | 'token') => {
29 | if (type === 'transaction') {
30 | return `https://signator.io/view?ipfs=${id}`;
31 | }
32 |
33 | throw new Error('Not implemented');
34 | }
35 | };
36 |
37 | return {
38 | readOnly: true,
39 | name: networkId === 's-tn' ? 'Snapshot (testnet)' : 'Snapshot',
40 | avatar: '',
41 | currentUnit: 'second',
42 | chainId: l1ChainId,
43 | baseChainId: l1ChainId,
44 | currentChainId: l1ChainId,
45 | hasReceive: false,
46 | supportsSimulation: false,
47 | managerConnectors: [],
48 | api,
49 | constants,
50 | helpers,
51 | actions: {
52 | getVotingPower: () => Promise.resolve([])
53 | }
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/networks/starknet/provider.ts:
--------------------------------------------------------------------------------
1 | import { RpcProvider } from 'starknet';
2 |
3 | export function createProvider(nodeUrl: string) {
4 | return new RpcProvider({
5 | nodeUrl
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 | import Home from '@/views/Home.vue';
3 | import Space from '@/views/Space.vue';
4 | import SpaceOverview from '@/views/Space/Overview.vue';
5 | import SpaceProposals from '@/views/Space/Proposals.vue';
6 | import SpaceSearch from '@/views/Space/Search.vue';
7 | import SpaceSettings from '@/views/Space/Settings.vue';
8 | import SpaceEditSettings from '@/views/Space/EditSettings.vue';
9 | import SpaceTreasury from '@/views/Space/Treasury.vue';
10 | import SpaceDelegates from '@/views/Space/Delegates.vue';
11 | import Editor from '@/views/Editor.vue';
12 | import Proposal from '@/views/Proposal.vue';
13 | import ProposalOverview from '@/views/Proposal/Overview.vue';
14 | import ProposalVotes from '@/views/Proposal/Votes.vue';
15 | import User from '@/views/User.vue';
16 | import Create from '@/views/Create.vue';
17 | import Settings from '@/views/Settings.vue';
18 | import Contacts from '@/views/Settings/Contacts.vue';
19 | import Explore from '@/views/Explore.vue';
20 | import SettingsSpaces from '@/views/Settings/Spaces.vue';
21 | import Apps from '@/views/Apps.vue';
22 | import App from '@/views/App.vue';
23 |
24 | const { mixpanel } = useMixpanel();
25 |
26 | const routes: any[] = [
27 | { path: '/', name: 'home', component: Home },
28 | {
29 | path: '/:id',
30 | name: 'space',
31 | component: Space,
32 | children: [
33 | { path: '', name: 'space-overview', component: SpaceOverview },
34 | { path: 'proposals', name: 'space-proposals', component: SpaceProposals },
35 | { path: 'search', name: 'space-search', component: SpaceSearch },
36 | { path: 'settings', name: 'space-settings', component: SpaceSettings },
37 | { path: 'edit-settings', name: 'space-edit-settings', component: SpaceEditSettings },
38 | { path: 'treasury', name: 'space-treasury', component: SpaceTreasury },
39 | { path: 'delegates', name: 'space-delegates', component: SpaceDelegates }
40 | ]
41 | },
42 | {
43 | path: '/:id/create/:key?',
44 | name: 'editor',
45 | component: Editor
46 | },
47 | {
48 | path: '/:space/proposal/:id?',
49 | name: 'proposal',
50 | component: Proposal,
51 | children: [
52 | { path: '', name: 'proposal-overview', component: ProposalOverview },
53 | { path: 'votes', name: 'proposal-votes', component: ProposalVotes }
54 | ]
55 | },
56 | { path: '/profile/:id', name: 'user', component: User },
57 | { path: '/create', name: 'create', component: Create },
58 | {
59 | path: '/settings',
60 | name: 'settings',
61 | component: Settings,
62 | children: [
63 | { path: '', name: 'settings-spaces', component: SettingsSpaces },
64 | { path: 'contacts', name: 'settings-contacts', component: Contacts }
65 | ]
66 | },
67 | { path: '/explore', name: 'explore', component: Explore },
68 | { path: '/apps', name: 'apps', component: Apps },
69 | { path: '/apps/:id', name: 'app', component: App }
70 | ];
71 |
72 | const router = createRouter({
73 | history: createWebHashHistory(),
74 | routes,
75 | scrollBehavior(to, from, savedPosition) {
76 | if (savedPosition) return savedPosition;
77 | if (to.params.retainScrollPosition) return {};
78 | if (to.hash) {
79 | const position = { selector: to.hash };
80 | return { el: position };
81 | }
82 | return { top: 0 };
83 | }
84 | });
85 |
86 | router.afterEach(to => {
87 | mixpanel.track_pageview({
88 | page_name: to.name,
89 | page_path: to.path
90 | });
91 | });
92 |
93 | export default router;
94 |
--------------------------------------------------------------------------------
/src/stores/contacts.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import type { Contact } from '@/types';
3 | import pkg from '../../package.json';
4 |
5 | export const useContactsStore = defineStore('contacts', {
6 | state: () => ({
7 | contacts: useStorage(`${pkg.name}.contacts`, [] as Contact[])
8 | }),
9 | actions: {
10 | saveContact(payload: Contact) {
11 | const contact = this.contacts.find(contact => contact.address === payload.address);
12 | if (contact) {
13 | Object.entries(payload).map(([key, value]) => {
14 | contact[key] = value;
15 | });
16 | } else {
17 | this.contacts.unshift(payload);
18 | }
19 | },
20 | deleteContact(address: string) {
21 | this.contacts = this.contacts.filter(contact => contact.address !== address);
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/stores/meta.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { NetworkID } from '@/types';
3 | import { getProvider } from '@/helpers/provider';
4 | import { evmNetworks, getNetwork } from '@/networks';
5 | import { METADATA } from '@/networks/evm';
6 |
7 | export const useMetaStore = defineStore('meta', () => {
8 | const currentTs = ref(new Map());
9 | const currentBlocks = ref(new Map());
10 |
11 | function getCurrent(networkId: NetworkID): number | undefined {
12 | if (evmNetworks.includes(networkId)) return currentBlocks.value.get(networkId);
13 | return currentTs.value.get(networkId);
14 | }
15 |
16 | async function fetchBlock(networkId: NetworkID) {
17 | if (currentBlocks.value.get(networkId)) return;
18 |
19 | const provider = getProvider(getNetwork(networkId).currentChainId);
20 |
21 | try {
22 | const blockNumber = await provider.getBlockNumber();
23 | currentBlocks.value.set(networkId, blockNumber);
24 | currentTs.value.set(networkId, Math.floor(Date.now() / 1e3));
25 | } catch (e) {
26 | console.error(e);
27 | }
28 | }
29 |
30 | function getCurrentFromDuration(networkId: NetworkID, duration: number) {
31 | const network = getNetwork(networkId);
32 |
33 | if (network.currentUnit === 'second') return duration;
34 |
35 | return Math.round(duration / METADATA[networkId].blockTime);
36 | }
37 |
38 | function getDurationFromCurrent(networkId: NetworkID, current: number) {
39 | const network = getNetwork(networkId);
40 |
41 | if (network.currentUnit === 'second') return current;
42 |
43 | return Math.round(current * METADATA[networkId].blockTime);
44 | }
45 |
46 | function getTsFromCurrent(networkId: NetworkID, current: number) {
47 | if (!evmNetworks.includes(networkId)) return current;
48 |
49 | const networkBlockNum = currentBlocks.value.get(networkId) || 0;
50 | const blockDiff = networkBlockNum - current;
51 |
52 | return (currentTs.value.get(networkId) || 0) - METADATA[networkId].blockTime * blockDiff;
53 | }
54 |
55 | return {
56 | getCurrent,
57 | fetchBlock,
58 | getCurrentFromDuration,
59 | getDurationFromCurrent,
60 | getTsFromCurrent
61 | };
62 | });
63 |
--------------------------------------------------------------------------------
/src/stores/spaces.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { getNetwork } from '@/networks';
3 | import { NetworkID, Space } from '@/types';
4 | import pkg from '../../package.json';
5 |
6 | export const useSpacesStore = defineStore('spaces', () => {
7 | const metaStore = useMetaStore();
8 | const { mixpanel } = useMixpanel();
9 | const starredSpacesIds = useStorage(`${pkg.name}.spaces-starred`, [] as string[]);
10 | const starredSpacesData = ref([] as Space[]);
11 |
12 | const {
13 | loading,
14 | loaded,
15 | networksMap,
16 | spaces,
17 | spacesMap,
18 | hasMoreSpaces,
19 | getSpaces,
20 | fetch,
21 | fetchMore
22 | } = useSpaces();
23 |
24 | const starredSpacesMap = computed(
25 | () => new Map(starredSpacesData.value.map(space => [`${space.network}:${space.id}`, space]))
26 | );
27 |
28 | const starredSpaces = computed({
29 | get() {
30 | return starredSpacesIds.value
31 | .map(id => starredSpacesMap.value.get(id))
32 | .filter(Boolean) as Space[];
33 | },
34 | set(spaces: Space[]) {
35 | starredSpacesIds.value = spaces.map(space => `${space.network}:${space.id}`);
36 | }
37 | });
38 |
39 | async function fetchSpace(spaceId: string, networkId: NetworkID) {
40 | await metaStore.fetchBlock(networkId);
41 |
42 | const network = getNetwork(networkId);
43 |
44 | const space = await network.api.loadSpace(spaceId);
45 | if (!space) return;
46 |
47 | networksMap.value[networkId].spaces = {
48 | ...networksMap.value[networkId].spaces,
49 | [spaceId]: space
50 | };
51 | }
52 |
53 | function toggleSpaceStar(id: string) {
54 | const alreadyStarred = starredSpacesIds.value.includes(id);
55 |
56 | if (alreadyStarred) {
57 | starredSpacesIds.value = starredSpacesIds.value.filter((spaceId: string) => spaceId !== id);
58 | } else {
59 | starredSpacesIds.value = [id, ...starredSpacesIds.value];
60 | }
61 |
62 | mixpanel.track('Set space favorite', {
63 | space: id,
64 | favorite: !alreadyStarred
65 | });
66 | }
67 |
68 | watch(
69 | starredSpacesIds,
70 | async (currentIds, previousIds) => {
71 | const newIds = !previousIds
72 | ? currentIds
73 | : currentIds.filter((id: string) => !previousIds.includes(id));
74 |
75 | const spaces = await getSpaces({
76 | id_in: newIds
77 | });
78 |
79 | starredSpacesData.value = [...starredSpacesData.value, ...spaces];
80 | },
81 | { immediate: true }
82 | );
83 |
84 | return {
85 | starredSpacesIds,
86 | starredSpaces,
87 | loading,
88 | loaded,
89 | networksMap,
90 | spaces,
91 | spacesMap,
92 | hasMoreSpaces,
93 | fetch,
94 | fetchMore,
95 | fetchSpace,
96 | toggleSpaceStar
97 | };
98 | });
99 |
--------------------------------------------------------------------------------
/src/stores/ui.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { getNetwork } from '@/networks';
3 | import { lsSet, lsGet } from '@/helpers/utils';
4 | import { NotificationType, NetworkID } from '@/types';
5 |
6 | type Notification = {
7 | id: string;
8 | type: NotificationType;
9 | message: string;
10 | };
11 |
12 | type PendingTransaction = {
13 | networkId: NetworkID;
14 | txId: string;
15 | createdAt: number;
16 | };
17 |
18 | const PENDING_TRANSACTIONS_TIMEOUT = 10 * 60 * 1000;
19 | const PENDING_TRANSACTIONS_STORAGE_KEY = 'pendingTransactions';
20 |
21 | function updateStorage(pendingTransactions: PendingTransaction[]) {
22 | lsSet(PENDING_TRANSACTIONS_STORAGE_KEY, pendingTransactions);
23 | }
24 |
25 | export const useUiStore = defineStore('ui', {
26 | state: () => ({
27 | sidebarOpen: false,
28 | notifications: [] as Notification[],
29 | pendingTransactions: [] as PendingTransaction[],
30 | pendingVotes: {} as Record
31 | }),
32 | actions: {
33 | async toggleSidebar() {
34 | this.sidebarOpen = !this.sidebarOpen;
35 | },
36 | addNotification(type: NotificationType, message: string, timeout = 5000) {
37 | const id = crypto.randomUUID();
38 |
39 | this.notifications.push({
40 | id,
41 | type,
42 | message
43 | });
44 |
45 | setTimeout(() => this.dismissNotification(id), timeout);
46 | },
47 | dismissNotification(id: string) {
48 | this.notifications = this.notifications.filter(notification => notification.id !== id);
49 | },
50 | async addPendingTransaction(txId: string, networkId: NetworkID) {
51 | this.pendingTransactions.push({
52 | networkId,
53 | txId,
54 | createdAt: Date.now()
55 | });
56 | updateStorage(this.pendingTransactions);
57 |
58 | try {
59 | await getNetwork(networkId).helpers.waitForTransaction(txId);
60 | } finally {
61 | this.pendingTransactions = this.pendingTransactions.filter(el => el.txId !== txId);
62 | updateStorage(this.pendingTransactions);
63 | }
64 | },
65 | async restorePendingTransactions() {
66 | const persistedTransactions = lsGet(PENDING_TRANSACTIONS_STORAGE_KEY, []);
67 |
68 | this.pendingTransactions = persistedTransactions.filter(
69 | tx => tx.createdAt && tx.createdAt + PENDING_TRANSACTIONS_TIMEOUT > Date.now()
70 | );
71 |
72 | if (persistedTransactions.length !== this.pendingTransactions.length) {
73 | updateStorage(this.pendingTransactions);
74 | }
75 |
76 | this.pendingTransactions.forEach(async ({ networkId, txId }) => {
77 | try {
78 | await getNetwork(networkId).helpers.waitForTransaction(txId);
79 | } finally {
80 | this.pendingTransactions = this.pendingTransactions.filter(el => el.txId !== txId);
81 | updateStorage(this.pendingTransactions);
82 | }
83 | });
84 | },
85 | async addPendingVote(proposalId: string) {
86 | this.pendingVotes[proposalId] = true;
87 | }
88 | }
89 | });
90 |
--------------------------------------------------------------------------------
/src/stores/users.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia';
2 | import { getNetwork } from '@/networks';
3 | import type { NetworkID, User } from '@/types';
4 |
5 | type UserRecord = {
6 | loading: boolean;
7 | loaded: boolean;
8 | user: User | null;
9 | };
10 |
11 | export const useUsersStore = defineStore('users', {
12 | state: () => ({
13 | users: {} as Record
14 | }),
15 | getters: {
16 | getUser: state => {
17 | return (userId: string) => {
18 | return state.users[userId]?.user ?? null;
19 | };
20 | }
21 | },
22 | actions: {
23 | async fetchUser(userId: string, networkId: NetworkID) {
24 | if (this.getUser(userId)) return;
25 |
26 | this.users[userId] = {
27 | loading: false,
28 | loaded: false,
29 | user: null
30 | };
31 |
32 | const record = toRef(this.users, userId) as Ref;
33 | record.value.loading = false;
34 |
35 | record.value.user = await getNetwork(networkId).api.loadUser(userId);
36 | record.value.loaded = true;
37 | record.value.loading = false;
38 | }
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/src/views/App.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Apps
22 |
23 |
24 |
25 |
26 |
38 |
39 |
40 |
44 |
45 |
How it works
46 |
47 |
48 |
49 |
Get started
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ app.author }}
57 |
58 |
65 |
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/views/Apps.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/views/Explore.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
38 |
39 |
40 |
Learn more
41 |
42 |
47 |
52 |
57 |
62 |
71 |
72 |
73 |
74 |
Join the community
75 |
86 |
87 | © {{ new Date().getFullYear() }} Snapshot Labs
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
18 |
19 |
--------------------------------------------------------------------------------
/src/views/Settings/Contacts.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
37 |
38 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/views/Settings/Spaces.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/views/Space.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/views/Space/Delegates.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
23 |
29 |
30 |
--------------------------------------------------------------------------------
/src/views/Space/Search.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
84 |
85 |
--------------------------------------------------------------------------------
/src/views/User.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
{{ shortenAddress(user.id) }}
36 |
37 | {{ _n(user.proposal_count) }} proposals ·
38 | {{ _n(user.vote_count) }} votes
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 |
3 | module.exports = {
4 | future: {
5 | hoverOnlyWhenSupported: true
6 | },
7 | content: ['./index.html', './src/**/*.{js,ts,vue}'],
8 | darkMode: 'class',
9 | theme: {
10 | extend: {
11 | borderColor: {
12 | DEFAULT: 'rgba(var(--border), )'
13 | },
14 | colors: {
15 | // IMPORTANT: Color variables that require opacity modifiers must be defined
16 | // without space function and opacity value. They can be recognized by the
17 | // placeholder. See: https://tailwindcss.com/docs/customizing-colors#using-css-variables
18 | transparent: 'transparent',
19 |
20 | // backgrounds
21 | 'skin-bg': 'rgba(var(--bg), )',
22 | 'skin-block-bg': 'rgba(var(--block-bg), )',
23 | 'skin-input-bg': 'rgba(var(--input-bg), )',
24 | 'skin-hover-bg': 'rgba(var(--hover-bg), )',
25 | 'skin-active-bg': 'rgba(var(--active-bg), )',
26 | 'skin-border': 'rgba(var(--border), )',
27 |
28 | // text
29 | 'skin-heading': 'rgba(var(--heading), )',
30 | 'skin-link': 'rgba(var(--link), )',
31 | 'skin-text': 'rgba(var(--text), )',
32 | 'skin-content': 'var(--content)',
33 |
34 | // accents
35 | 'skin-primary': 'rgba(var(--primary), )',
36 | 'skin-accent-foreground': 'rgba(var(--accent-foreground), )',
37 | 'skin-danger': 'rgba(var(--danger), )',
38 | 'skin-success': 'rgba(var(--success), )',
39 |
40 | 'skin-accent-hover': 'var(--accent-hover)',
41 | 'skin-accent-active': 'var(--accent-active)',
42 | 'skin-danger-border': 'var(--danger-border)',
43 | 'skin-danger-hover': 'var(--danger-hover)',
44 | 'skin-danger-active': 'var(--danger-active)',
45 | 'skin-success-border': 'var(--success-border)',
46 | 'skin-success-hover': 'var(--success-hover)',
47 | 'skin-success-active': 'var(--success-active)'
48 | },
49 | animation: {
50 | 'pulse-fast': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite'
51 | }
52 | },
53 | screens: {
54 | xs: '420px',
55 | sm: '544px',
56 | md: '768px',
57 | lg: '1012px',
58 | xl: '1280px'
59 | },
60 | spacing: {
61 | 0: '0px',
62 | 1: '4px',
63 | 2: '8px',
64 | 2.5: '14px',
65 | 3: '16px',
66 | 4: '24px',
67 | 5: '32px',
68 | 6: '40px',
69 | 7: '48px',
70 | 8: '64px'
71 | },
72 | fontFamily: {
73 | serif: ['"Calibre", Helvetica, Arial, sans-serif']
74 | },
75 | fontSize: {
76 | '2xl': ['34px'],
77 | xl: ['28px'],
78 | lg: ['22px'],
79 | md: ['20px'],
80 | base: ['18px'],
81 | sm: ['17px'],
82 | xs: ['13px']
83 | }
84 | }
85 | };
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "isolatedModules": true,
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "lib": ["esnext", "dom"],
13 | "importHelpers": true,
14 | "allowSyntheticDefaultImports": true,
15 | "allowJs": true,
16 | "baseUrl": ".",
17 | "types": ["vite/client", "unplugin-icons/types/vue"],
18 | "paths": {
19 | "@/*": ["src/*"]
20 | },
21 | "noImplicitAny": false,
22 | "skipLibCheck": true,
23 | "useUnknownInCatchVariables": false
24 | },
25 | "vueCompilerOptions": {
26 | "narrowingTypesInInlineHandlers": true
27 | },
28 | "include": [
29 | "src/**/*.ts",
30 | "src/**/*.d.ts",
31 | "src/**/*.tsx",
32 | "src/**/*.vue",
33 | "auto-imports.d.ts",
34 | "components.d.ts"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { defineConfig } from 'vite';
3 | import vue from '@vitejs/plugin-vue';
4 | import AutoImport from 'unplugin-auto-import/vite';
5 | import Components from 'unplugin-vue-components/vite';
6 | import Icons from 'unplugin-icons/vite';
7 | import IconsResolver from 'unplugin-icons/resolver';
8 | import { FileSystemIconLoader } from 'unplugin-icons/loaders';
9 | import visualizer from 'rollup-plugin-visualizer';
10 | import inject from '@rollup/plugin-inject';
11 |
12 | const ELECTRON = process.env.ELECTRON || false;
13 |
14 | const target = ['esnext'];
15 |
16 | export default defineConfig({
17 | base: ELECTRON ? path.resolve(__dirname, './dist') : undefined,
18 | define: {
19 | 'process.env': process.env
20 | },
21 | plugins: [
22 | vue(),
23 | AutoImport({
24 | imports: ['vue', 'vue-router', '@vueuse/core'],
25 | dirs: ['./src/composables', './src/stores'],
26 | eslintrc: {
27 | enabled: true
28 | }
29 | }),
30 | Components({
31 | directoryAsNamespace: true,
32 | resolvers: [
33 | IconsResolver({
34 | customCollections: ['c'],
35 | alias: {
36 | h: 'heroicons-outline',
37 | s: 'heroicons-solid'
38 | }
39 | })
40 | ]
41 | }),
42 | visualizer({
43 | filename: './dist/stats.html',
44 | template: 'sunburst',
45 | gzipSize: true
46 | }),
47 | Icons({
48 | compiler: 'vue3',
49 | iconCustomizer(collection, icon, props) {
50 | props.width = '20px';
51 | props.height = '20px';
52 | },
53 | customCollections: {
54 | c: FileSystemIconLoader('./src/assets/icons', svg =>
55 | svg.replace(/^