├── .github ├── gh-pages └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── Todo ├── cli ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── example.json ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── core ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── endpoint_dump.ts │ ├── endpoints.ts │ ├── fetch │ │ ├── acala.ts │ │ ├── index.ts │ │ ├── moonbeam.ts │ │ └── substrate.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── tests │ └── index.test.ts └── tsconfig.json ├── package-lock.json └── ui ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── components │ ├── About │ │ └── index.tsx │ ├── AccountIcon │ │ └── index.tsx │ ├── Accounts │ │ ├── AccountListSettings.tsx │ │ ├── AccountsList.tsx │ │ └── index.tsx │ ├── App │ │ └── index.tsx │ ├── Assets │ │ ├── AssetList.tsx │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── Modal │ │ └── index.tsx │ ├── ModalBox │ │ └── index.tsx │ └── Networks │ │ ├── ManualNetworkInput.tsx │ │ ├── NetworkLookup.tsx │ │ ├── NetworkSettings.tsx │ │ ├── NetworksList.tsx │ │ └── index.tsx ├── hooks │ ├── clickOutside.ts │ ├── useBackgroundFetch.ts │ └── usePrevious.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── store │ ├── index.tsx │ ├── reducer │ │ └── AppReducer.ts │ └── store.d.ts ├── utils │ ├── constants.ts │ ├── localStorage │ │ └── index.ts │ ├── styles.ts │ └── validators.ts └── views │ └── Dashboard │ ├── Sidebar.tsx │ └── index.tsx ├── tailwind.config.js └── tsconfig.json /.github/gh-pages: -------------------------------------------------------------------------------- 1 | name: Deploy React Application 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [ master ] 8 | 9 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 10 | jobs: 11 | build_test: 12 | # The type of runner that the job will run on 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [12.x] # We will deploy with only one version of node 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: npm ci, build and test 28 | run: | 29 | cd ./ui 30 | npm run build --if-present 31 | - name: deploy to gh-pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 35 | publish_dir: ./ui/build -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy React Application 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | build_test: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] # We will deploy with only one version of node 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Build Core Project 30 | run: | 31 | cd ./core 32 | npm install 33 | npm run build 34 | - name: Build React Project 35 | run: | 36 | cd ./ui 37 | npm install 38 | npm link ../core 39 | DISABLE_ESLINT_PLUGIN=true npm run build 40 | - name: deploy to gh-pages 41 | uses: peaceiris/actions-gh-pages@v3 42 | with: 43 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 44 | publish_dir: ./ui/build 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # polkadot-portfolio 🤑 2 | 3 | - Discover the depth of *all* of your bags of value bearing tokens in the complicated world of Polkadot 🔴. 4 | - Easily works with *any substrate-based network* ⛓. 5 | - Read-only: no-signing, no private keys involved. Easily add any account via the public key 🔑. 6 | 7 | Screen Shot 2022-08-09 at 08 21 40 8 | 9 | 10 | ### Sponsoring 11 | 12 | Made by [@kianenigma](https://sub.id/1eTPAR2TuqLyidmPT9rMmuycHVm9s9czu78sePqg2KHMDrE) and [@HoseinEmrani](https://github.com/HoseinEmrani) (13HCnHKxS26NreRYiH5LCeyTQzQvXDCcS9hC5G6nhnSj1KFS) 13 | -------------------------------------------------------------------------------- /Todo: -------------------------------------------------------------------------------- 1 | UI: 2 | - Fetching Coin Data: 3 | [] Live coin data 4 | 5 | - Networks and Accounts: 6 | [x] Auto complete for networks: We should have a list of hardcoded networks (copy pasta from polkadot.js/apps), and upon adding a new network, you should be able to just type “Polkadot” and it should potentially suggest a known ws endpoint for it. 7 | [] Networks should have icons (hardcoded) 8 | [x] Accounts should have icon as well (https://github.com/polkadot-js/ui/tree/master/packages/react-identicon#readme) 9 | [x] Validate accounts on input 10 | 11 | - Actions on Token Table: 12 | [] Filtering small amounts 13 | [] Filtering by Token Name 14 | [] Filtering table by values 15 | [] Grouping stuff by headliners (harder) 16 | [] Add unit test. 17 | 18 | - [ ] UI Network Preludes: ALL, DOTSama, Polkadot, Kusama 19 | - [ ] Account Import from Extensions 20 | 21 | BUGS: 22 | - [] Loading is not accurate yet. It hides before fetching finishes. 23 | - [] On networks add/remove all assets sometimes doesn't load up. 24 | 25 | CORE: 26 | [] Provide API to fetch coins data from various sources and keep them up to date 27 | [] Detect address format (ETH + BTC) 28 | [x] Dictionary for network names to WSS urls (to be maintained by community) (Re-export from polkadot-js/app/endpoints) 29 | [] Dictionary for asset sources to be exported from Core 30 | [] Native assets and derivatives should be distinguished (task for Kian) 31 | [x] Clean up Core/UI related functions from class 32 | [] Add unit test 33 | [] Remove TS-ignore/Ts-No checks. 34 | [x] Refactor core with better design 35 | 36 | CLI: 37 | [x] Move summary from Core to CLI 38 | 39 | Delivery: 40 | [] README.md + CONTRIBUTING.md 41 | [] Github deployment soon so we can shill it a bit and also request tips 42 | -------------------------------------------------------------------------------- /cli/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /cli/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/ban-ts-comment": "warn", 15 | "indent": [ 16 | "error", 17 | "tab" 18 | ], 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | accounts*.json 3 | 4 | # dependencies 5 | ./node_modules 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /cli/.prettierrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": true, 6 | "printWidth": 100, 7 | "singleQuote": true, 8 | "trailingComma": "none", 9 | "jsxBracketSameLine": true 10 | } 11 | -------------------------------------------------------------------------------- /cli/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "stashes": [ 3 | ["rcUEwGx4TfAcwtsThSJcpY1shzUfHSMfqV4jULZEgge3uYD", "account1"], 4 | ["HKKT5DjFaUE339m7ZWS2yutjecbUpBcDQZHw2EF7SFqSFJH", "RMRK-MultiSIG"], 5 | ["HL8bEp8YicBdrUmJocCAWVLKUaR2dd1y6jnD934pbre3un1", "ksm-ctrl"], 6 | ["Eqm6aUjJDEWGAPfvFNpQcDgTSL44SuTCo1uFX7RwBwic74h", "ksm-2"], 7 | ["0x8E9D48d936768237D6aD9378026bF4Bc7ECBC4bc", "eth"], 8 | ["16FH7GKMqRY6QSYFF1doUL5D9uYwhbNd7rkuu6hAtDDTnbzE", "account1"], 9 | ["Cb2QccEAM38pjwmcHHTTuTukUobTHwhakKH4kBo4k8Vur8o", "account2"] 10 | ], 11 | "networks": [ 12 | "wss://kusama-rpc.polkadot.io", 13 | "wss://rpc.polkadot.io", 14 | "wss://statemine-rpc.polkadot.io", 15 | 16 | "wss://karura-rpc-0.aca-api.network", 17 | "wss://acala-polkadot.api.onfinality.io/public-ws", 18 | 19 | "wss://khala-api.phala.network/ws", 20 | 21 | "wss://rpc.astar.network", 22 | "wss://rpc.parallel.fi", 23 | 24 | "wss://wss.api.moonbeam.network", 25 | "wss://wss.moonriver.moonbeam.network" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polkadot-portfolio-cli", 3 | "version": "0.1.1", 4 | "description": "CLI App to scrape the portfolio fo an account in the Polkadot ecosystem.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "dev": "./node_modules/.bin/nodemon --exec ./node_modules/.bin/ts-node ./src/index.ts", 9 | "start": "./node_modules/.bin/ts-node ./src/index.ts", 10 | "update": "./node_modules/.bin/ncu -u && yarn", 11 | "lint": "eslint . --ext .ts --fix", 12 | "lint:fix": "eslint --fix", 13 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", 14 | "build": "./node_modules/.bin/tsc --pretty" 15 | }, 16 | "author": "@kianenigma", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@babel/cli": "^7.17.6", 20 | "@babel/core": "^7.17.8", 21 | "@babel/preset-typescript": "^7.16.7", 22 | "@types/bn.js": "^5.1.0", 23 | "@types/currency-formatter": "^1.5.1", 24 | "@types/node": "^17.0.23", 25 | "@types/yargs": "^17.0.10", 26 | "@typescript-eslint/eslint-plugin": "^5.17.0", 27 | "@typescript-eslint/parser": "^5.17.0", 28 | "eslint": "^8.12.0", 29 | "eslint-config-prettier": "^8.5.0", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "nodemon": "^2.0.15", 32 | "npm-check-updates": "12.5.7", 33 | "prettier": "^2.7.1", 34 | "ts-loader": "^9.2.8", 35 | "ts-node": "^10.7.0", 36 | "typescript": "4.6.3", 37 | "typescript-formatter": "^7.2.2" 38 | }, 39 | "dependencies": { 40 | "polkadot-portfolio-core": "../core", 41 | "bn.js": "4.12.0", 42 | "currency-formatter": "1.5.9", 43 | "yargs": "^17.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { tickerPrice, scrape, Asset, ApiPromise, makeApi } from 'polkadot-portfolio-core'; 3 | import * as BN from 'bn.js'; 4 | import * as currencyFormatter from 'currency-formatter'; 5 | import yargs from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | 8 | interface AccountsConfig { 9 | networks: string[]; 10 | stashes: [string, string][]; 11 | } 12 | 13 | const optionsPromise = yargs(hideBin(process.argv)).option('accounts', { 14 | alias: 'a', 15 | type: 'string', 16 | description: 'path to a JSON file with your accounts in it.', 17 | required: true 18 | }).argv; 19 | 20 | interface AssetAndRatio { 21 | asset: Asset; 22 | ratio: number; 23 | } 24 | 25 | class Summary { 26 | assets: Map; 27 | total_eur_value: number; 28 | 29 | constructor(input_assets: Asset[]) { 30 | const assets: Map = new Map(); 31 | for (const asset of input_assets) { 32 | if (!asset.amount.isZero()) { 33 | if (assets.has(asset.ticker)) { 34 | const { asset: cumulative, ratio } = assets.get(asset.ticker) || { 35 | asset: asset, 36 | ratio: 0 37 | }; 38 | cumulative.amount = cumulative.amount.add(asset.amount); 39 | assets.set(asset.ticker, { asset: cumulative, ratio: 0 }); 40 | } else { 41 | const copy: Asset = new Asset({ ...asset }); 42 | assets.set(asset.ticker, { asset: copy, ratio: 0 }); 43 | } 44 | } 45 | } 46 | 47 | // compute sum of EUR-value in the entire map, and assign new ratio to each. 48 | let total_eur_value = 0; 49 | assets.forEach(({ asset }) => (total_eur_value = total_eur_value + asset.euroValue())); 50 | 51 | for (const asset_id of assets.keys()) { 52 | // just a wacky way to tell TS that the map def. contains `asset_id`: 53 | // https://typescript-eslint.io/rules/no-non-null-assertion/ 54 | // https://linguinecode.com/post/how-to-solve-typescript-possibly-undefined-value 55 | const { asset, ratio: _prev_raio } = assets.get(asset_id)!; 56 | const new_ratio = asset.euroValue() / total_eur_value; 57 | assets.set(asset_id, { asset, ratio: new_ratio }); 58 | } 59 | 60 | this.total_eur_value = total_eur_value; 61 | this.assets = assets; 62 | } 63 | 64 | stringify(): string { 65 | let ret = ''; 66 | const sorted = Array.from(this.assets.entries()) 67 | .sort((a, b) => a[1].ratio - b[1].ratio) 68 | .reverse(); 69 | for (const [_, { asset: sum_asset, ratio }] of sorted) { 70 | ret += `🎁 sum of ${sum_asset.ticker}: ${formatAmount(sum_asset)}, ${(ratio * 100).toFixed( 71 | 2 72 | )}% of total [unit price = ${sum_asset.price}].\n`; 73 | } 74 | ret += `💰 total EUR value: ${currencyFormatter.format(this.total_eur_value, { 75 | locale: 'nl-NL' 76 | })}\n`; 77 | return ret; 78 | } 79 | } 80 | 81 | function formatAmount(asset: Asset): string { 82 | const formatNumber = (x: BN) => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 83 | const token_amount = `${formatNumber(asset.decimal())}.${asset 84 | .fraction() 85 | .toString() 86 | .padStart(3, '0')}`; 87 | const eur_amount = asset.euroValue(); 88 | return `${token_amount} - ${currencyFormatter.format(eur_amount, { locale: 'nl-NL' })}`; 89 | } 90 | 91 | function stringifyAsset(asset: Asset): string { 92 | return `[${asset.transferrable ? '🍺' : '🔐'}][${asset.ticker}] ${asset.name}: ${formatAmount( 93 | asset 94 | )}`; 95 | } 96 | 97 | export async function main() { 98 | const apiRegistry: Map = new Map(); 99 | // initialize `apiRegistry`. 100 | const options = await optionsPromise; 101 | const accountConfig: AccountsConfig = JSON.parse(readFileSync(options.accounts).toString()); 102 | 103 | // connect to all api endpoints. 104 | await Promise.all( 105 | accountConfig.networks.map(async (uri: string) => { 106 | const { api } = await makeApi(uri); 107 | apiRegistry.set(uri, api); 108 | console.log( 109 | `⛓ Connected to ${uri} / tokens ${api.registry.chainTokens} / [ss58: ${api.registry.chainSS58}]` 110 | ); 111 | apiRegistry.set(uri, api); 112 | }) 113 | ); 114 | 115 | let allAssets: Asset[] = []; 116 | for (const networkWs of accountConfig.networks) { 117 | const api = apiRegistry.get(networkWs)!; 118 | for (const [account] of accountConfig.stashes) { 119 | const accountAssets = await scrape(account, api); 120 | allAssets = allAssets.concat(accountAssets); 121 | } 122 | } 123 | 124 | accountConfig.stashes.forEach(([account, name]) => { 125 | const accountAssets = allAssets.filter((a) => a.origin.account === account); 126 | console.log(`#########`); 127 | console.log(`# Summary of ${account} / ${name}:`); 128 | for (const asset of accountAssets) { 129 | console.log(stringifyAsset(asset)); 130 | } 131 | console.log(`#########`); 132 | }); 133 | 134 | for (const networkWs of accountConfig.networks) { 135 | const api = apiRegistry.get(networkWs)!; 136 | const chain = (await api.rpc.system.chain()).toString(); 137 | const chainAssets = allAssets.filter((a) => a.origin.chain === chain); 138 | console.log(`#########`); 139 | console.log(`# Summary of chain ${chain}:`); 140 | for (const asset of chainAssets) { 141 | console.log(stringifyAsset(asset)); 142 | } 143 | console.log(`#########`); 144 | } 145 | 146 | const finalSummary = new Summary(allAssets); 147 | console.log(`#########`); 148 | console.log(`# Final Summary:\n${finalSummary.stringify()}`); 149 | console.log(`#########`); 150 | } 151 | 152 | main() 153 | .catch(console.error) 154 | .finally(() => process.exit()); 155 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "allowJs": true, 7 | "rootDir": "./src", 8 | // Some compiling checks. 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | 17 | // dep resolution 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | "experimentalDecorators": true, 21 | "resolveJsonModule": true, 22 | 23 | "declaration": false, 24 | "emitDeclarationOnly": false, 25 | "outDir": "build", 26 | "declarationMap": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /core/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/ban-ts-comment": "warn", 15 | "indent": [ 16 | "error", 17 | "tab" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | key 3 | build 4 | accounts*.json 5 | .idea/ 6 | .vscode/ 7 | build/ 8 | .DS_Store 9 | *.tgz 10 | my-app* 11 | template/src/__tests__/__snapshots__/ 12 | lerna-debug.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | /.changelog 17 | .npm/ 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /core/.prettierrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": true, 6 | "printWidth": 100, 7 | "singleQuote": true, 8 | "trailingComma": "none", 9 | "jsxBracketSameLine": true 10 | } 11 | -------------------------------------------------------------------------------- /core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { '^.+\\.ts?$': 'ts-jest' }, 3 | testEnvironment: 'node', 4 | testRegex: '/tests/.*\\.(test|spec)?\\.(ts|tsx)$', 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 6 | }; 7 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polkadot-portfolio-core", 3 | "version": "0.1.1", 4 | "description": "Core engine to scrape the portfolio fo an account in the Polkadot ecosystem.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "clean": "./node_modules/.bin/rimraf build", 9 | "test": "./node_modules/.bin/jest", 10 | "dev": "./node_modules/.bin/tsc --pretty --declaration --watch", 11 | "update": "./node_modules/.bin/ncu -u && yarn", 12 | "lint": "eslint . --ext .ts --fix", 13 | "lint:fix": "eslint --fix", 14 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", 15 | "build": "npm run clean && ./node_modules/.bin/tsc --pretty --declaration" 16 | }, 17 | "author": "@kianenigma", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@babel/cli": "^7.17.6", 21 | "@babel/core": "^7.17.8", 22 | "@babel/preset-typescript": "^7.16.7", 23 | "@types/currency-formatter": "^1.5.1", 24 | "@types/jest": "^28.1.6", 25 | "@types/lodash": "^4.14.182", 26 | "@types/node": "^17.0.23", 27 | "@types/yargs": "^17.0.10", 28 | "@typescript-eslint/eslint-plugin": "^5.17.0", 29 | "@typescript-eslint/parser": "^5.17.0", 30 | "eslint": "^8.12.0", 31 | "eslint-config-prettier": "^8.5.0", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "jest": "^28.1.3", 34 | "nodemon": "^2.0.15", 35 | "npm-check-updates": "^16.0.0", 36 | "prettier": "^2.7.1", 37 | "rimraf": "^3.0.2", 38 | "ts-jest": "^28.0.7", 39 | "ts-loader": "^9.2.8", 40 | "ts-node": "^10.7.0", 41 | "typescript": "4.6.3", 42 | "typescript-formatter": "^7.2.2" 43 | }, 44 | "dependencies": { 45 | "@acala-network/types": "4.1.5", 46 | "@open-web3/orml-types": "2.0.1", 47 | "@polkadot/api": "^8.13.1", 48 | "@polkadot/util-crypto": "^10.1.1", 49 | "axios": "^0.26.1", 50 | "bn.js": "4.12.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/endpoint_dump.ts: -------------------------------------------------------------------------------- 1 | // Someday this fill shall be replaced with apps-config. 2 | 3 | export interface EndpointOption { 4 | dnslink?: string; 5 | genesisHash?: string; 6 | homepage?: string; 7 | isChild?: boolean; 8 | isDevelopment?: boolean; 9 | isDisabled?: boolean; 10 | isUnreachable?: boolean; 11 | linked?: EndpointOption[]; 12 | info?: string; 13 | paraId?: number; 14 | providers: Record; 15 | summary?: string; 16 | teleport?: number[]; 17 | text: string; 18 | } 19 | 20 | export interface LinkOption { 21 | dnslink?: string; 22 | genesisHash?: string; 23 | genesisHashRelay?: string; 24 | homepage?: string; 25 | isChild?: boolean; 26 | isDevelopment?: boolean; 27 | isLightClient?: boolean; 28 | isRelay?: boolean; 29 | isUnreachable?: boolean; 30 | isSpaced?: boolean; 31 | linked?: LinkOption[]; 32 | paraId?: number; 33 | summary?: string; 34 | teleport?: number[]; 35 | textBy: string; 36 | value: string; 37 | valueRelay?: string[]; 38 | } 39 | 40 | export const prodParasKusama: EndpointOption[] = [ 41 | { 42 | info: 'altair', 43 | homepage: 'https://centrifuge.io/altair', 44 | paraId: 2088, 45 | text: 'Altair', 46 | providers: { 47 | Centrifuge: 'wss://fullnode.altair.centrifuge.io', 48 | OnFinality: 'wss://altair.api.onfinality.io/public-ws' 49 | } 50 | }, 51 | { 52 | info: 'amplitude', 53 | homepage: 'https://pendulumchain.org/amplitude', 54 | paraId: 2124, 55 | text: 'Amplitude', 56 | isUnreachable: true, 57 | providers: {} // Working on making this live ASAP 58 | }, 59 | { 60 | info: 'bajun', 61 | homepage: 'https://ajuna.io', 62 | paraId: 2119, 63 | text: 'Bajun Network', 64 | providers: { 65 | AjunaNetwork: 'wss://rpc-parachain.bajun.network' 66 | } 67 | }, 68 | { 69 | info: 'basilisk', 70 | homepage: 'https://bsx.fi', 71 | paraId: 2090, 72 | text: 'Basilisk', 73 | providers: { 74 | HydraDX: 'wss://rpc-01.basilisk.hydradx.io', 75 | OnFinality: 'wss://basilisk.api.onfinality.io/public-ws', 76 | Dwellir: 'wss://basilisk-rpc.dwellir.com' 77 | } 78 | }, 79 | { 80 | info: 'bifrost', 81 | homepage: 'https://ksm.vtoken.io/?ref=polkadotjs', 82 | paraId: 2001, 83 | text: 'Bifrost', 84 | providers: { 85 | 'Liebi 0': 'wss://bifrost-rpc.liebi.com/ws', 86 | 'Liebi 1': 'wss://us.bifrost-rpc.liebi.com/ws', 87 | 'Liebi 2': 'wss://eu.bifrost-rpc.liebi.com/ws', 88 | OnFinality: 'wss://bifrost-parachain.api.onfinality.io/public-ws', 89 | Dwellir: 'wss://bifrost-rpc.dwellir.com' 90 | } 91 | }, 92 | { 93 | info: 'bitcountryPioneer', 94 | homepage: 'https://bit.country/?ref=polkadotjs', 95 | paraId: 2096, 96 | text: 'Bit.Country Pioneer', 97 | providers: { 98 | 'Bit.Country': 'wss://pioneer-1-rpc.bit.country', 99 | OnFinality: 'wss://pioneer.api.onfinality.io/public-ws' 100 | } 101 | }, 102 | { 103 | info: 'calamari', 104 | homepage: 'https://www.calamari.network/', 105 | paraId: 2084, 106 | text: 'Calamari', 107 | providers: { 108 | 'Manta Network': 'wss://ws.calamari.systems/', 109 | OnFinality: 'wss://calamari.api.onfinality.io/public-ws' 110 | } 111 | }, 112 | { 113 | info: 'shadow', 114 | homepage: 'https://crust.network/', 115 | paraId: 2012, 116 | text: 'Crust Shadow', 117 | providers: { 118 | Crust: 'wss://rpc-shadow.crust.network/' 119 | } 120 | }, 121 | { 122 | info: 'crab', 123 | homepage: 'https://crab.network', 124 | paraId: 2105, 125 | text: 'Darwinia Crab Parachain', 126 | providers: { 127 | Crab: 'wss://crab-parachain-rpc.darwinia.network/' 128 | } 129 | }, 130 | { 131 | info: 'dorafactory', 132 | homepage: 'https://dorafactory.org/kusama/', 133 | paraId: 2115, 134 | text: 'Dora Factory', 135 | providers: { 136 | DORA: 'wss://kusama.dorafactory.org' 137 | } 138 | }, 139 | { 140 | info: 'genshiro', 141 | homepage: 'https://genshiro.equilibrium.io', 142 | isUnreachable: true, // https://github.com/polkadot-js/apps/pull/6761 143 | paraId: 2024, 144 | text: 'Genshiro', 145 | providers: { 146 | Equilibrium: 'wss://node.genshiro.io' 147 | } 148 | }, 149 | { 150 | info: 'gm', 151 | isUnreachable: true, 152 | homepage: 'https://gmordie.com', 153 | paraId: 2123, 154 | text: 'GM Parachain', 155 | providers: { 156 | GMorDieDAO: 'wss://kusama.gmordie.com' 157 | } 158 | }, 159 | { 160 | info: 'imbue', 161 | homepage: 'https://imbue.network', 162 | paraId: 2121, 163 | text: 'Imbue Network', 164 | providers: { 165 | 'Imbue Network': 'wss://imbue-kusama.imbue.network' 166 | } 167 | }, 168 | { 169 | info: 'integritee', 170 | homepage: 'https://integritee.network', 171 | paraId: 2015, 172 | text: 'Integritee Network', 173 | providers: { 174 | Integritee: 'wss://kusama.api.integritee.network', 175 | OnFinality: 'wss://integritee-kusama.api.onfinality.io/public-ws' 176 | } 177 | }, 178 | { 179 | info: 'tinker', 180 | homepage: 'https://invarch.network/tinkernet', 181 | paraId: 2125, 182 | text: 'InvArch Tinkernet', 183 | providers: { 184 | 'InvArch Team': 'wss://tinker.invarch.network' 185 | } 186 | }, 187 | { 188 | info: 'kabocha', 189 | homepage: 'https://kabocha.network', 190 | paraId: 2113, 191 | text: 'Kabocha', 192 | providers: { 193 | JelliedOwl: 'wss://kabocha.jelliedowl.com' 194 | } 195 | }, 196 | { 197 | info: 'karura', 198 | homepage: 'https://acala.network/karura/join-karura', 199 | paraId: 2000, 200 | text: 'Karura', 201 | providers: { 202 | 'Acala Foundation 0': 'wss://karura-rpc-0.aca-api.network', 203 | 'Acala Foundation 1': 'wss://karura-rpc-1.aca-api.network', 204 | 'Acala Foundation 2': 'wss://karura-rpc-2.aca-api.network/ws', 205 | 'Acala Foundation 3': 'wss://karura-rpc-3.aca-api.network/ws', 206 | 'Polkawallet 0': 'wss://karura.polkawallet.io', 207 | OnFinality: 'wss://karura.api.onfinality.io/public-ws', 208 | Dwellir: 'wss://karura-rpc.dwellir.com' 209 | } 210 | }, 211 | { 212 | info: 'khala', 213 | homepage: 'https://phala.network/', 214 | paraId: 2004, 215 | text: 'Khala Network', 216 | providers: { 217 | Phala: 'wss://khala-api.phala.network/ws', 218 | OnFinality: 'wss://khala.api.onfinality.io/public-ws', 219 | Dwellir: 'wss://khala-rpc.dwellir.com', 220 | Pinknode: 'wss://public-rpc.pinknode.io/khala' 221 | } 222 | }, 223 | { 224 | info: 'kico', 225 | homepage: 'https://dico.io/', 226 | paraId: 2107, 227 | text: 'KICO', 228 | providers: { 229 | 'DICO Foundation': 'wss://rpc.kico.dico.io', 230 | 'DICO Foundation 2': 'wss://rpc.api.kico.dico.io' 231 | } 232 | }, 233 | { 234 | info: 'kilt', 235 | homepage: 'https://www.kilt.io/', 236 | paraId: 2086, 237 | text: 'KILT Spiritnet', 238 | providers: { 239 | 'KILT Protocol': 'wss://spiritnet.kilt.io/', 240 | OnFinality: 'wss://spiritnet.api.onfinality.io/public-ws', 241 | Dwellir: 'wss://kilt-rpc.dwellir.com' 242 | } 243 | }, 244 | { 245 | info: 'kintsugi', 246 | homepage: 'https://kintsugi.interlay.io/', 247 | paraId: 2092, 248 | text: 'Kintsugi BTC', 249 | providers: { 250 | 'Kintsugi Labs': 'wss://api-kusama.interlay.io/parachain', 251 | OnFinality: 'wss://kintsugi.api.onfinality.io/public-ws', 252 | Dwellir: 'wss://kintsugi-rpc.dwellir.com' 253 | } 254 | }, 255 | { 256 | info: 'kpron', 257 | homepage: 'http://apron.network/', 258 | isUnreachable: true, 259 | paraId: 2019, 260 | text: 'Kpron', 261 | providers: { 262 | Kpron: 'wss://kusama-kpron-rpc.apron.network/' 263 | } 264 | }, 265 | { 266 | info: 'listen', 267 | homepage: 'https://listen.io/', 268 | paraId: 2118, 269 | text: 'Listen Network', 270 | providers: { 271 | 'Listen Foundation 1': 'wss://rpc.mainnet.listen.io', 272 | 'Listen Foundation 2': 'wss://wss.mainnet.listen.io' 273 | } 274 | }, 275 | { 276 | info: 'litmus', 277 | homepage: 'https://kusama-crowdloan.litentry.com', 278 | paraId: 2106, 279 | isUnreachable: false, 280 | text: 'Litmus', 281 | providers: { 282 | Litentry: 'wss://rpc.litmus-parachain.litentry.io' 283 | } 284 | }, 285 | { 286 | info: 'loomNetwork', 287 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/5888 288 | homepage: 'https://loomx.io/', 289 | paraId: 2080, 290 | text: 'Loom Network', 291 | providers: { 292 | LoomNetwork: 'wss://kusama.dappchains.com' 293 | } 294 | }, 295 | { 296 | info: 'mangata', 297 | homepage: 'https://mangata.finance', 298 | paraId: 2110, 299 | text: 'Mangata', 300 | providers: { 301 | Mangata: 'wss://prod-kusama-collator-01.mangatafinance.cloud', 302 | OnFinality: 'wss://mangata-x.api.onfinality.io/public-ws' 303 | } 304 | }, 305 | { 306 | info: 'mars', 307 | homepage: 'https://www.aresprotocol.io/mars', 308 | paraId: 2008, 309 | text: 'Mars', 310 | providers: { 311 | AresProtocol: 'wss://wss.mars.aresprotocol.io' 312 | } 313 | }, 314 | { 315 | info: 'moonriver', 316 | homepage: 'https://moonbeam.network/networks/moonriver/', 317 | paraId: 2023, 318 | text: 'Moonriver', 319 | providers: { 320 | 'Moonbeam Foundation': 'wss://wss.api.moonriver.moonbeam.network', 321 | Blast: 'wss://moonriver.public.blastapi.io', 322 | Dwellir: 'wss://moonriver-rpc.dwellir.com', 323 | OnFinality: 'wss://moonriver.api.onfinality.io/public-ws', 324 | Pinknode: 'wss://public-rpc.pinknode.io/moonriver' 325 | // Pinknode: 'wss://rpc.pinknode.io/moonriver/explorer' // https://github.com/polkadot-js/apps/issues/7058 326 | } 327 | }, 328 | { 329 | info: 'heiko', 330 | homepage: 'https://parallel.fi', 331 | paraId: 2085, 332 | text: 'Parallel Heiko', 333 | providers: { 334 | OnFinality: 'wss://parallel-heiko.api.onfinality.io/public-ws', 335 | Parallel: 'wss://heiko-rpc.parallel.fi' 336 | } 337 | }, 338 | { 339 | info: 'heiko', 340 | homepage: 'https://parallel.fi', 341 | paraId: 2126, 342 | isUnreachable: true, 343 | text: 'Parallel Heiko 2', 344 | providers: {} 345 | }, 346 | { 347 | info: 'picasso', 348 | homepage: 'https://picasso.composable.finance/', 349 | paraId: 2087, 350 | text: 'Picasso', 351 | providers: { 352 | Composable: 'wss://picasso-rpc.composable.finance', 353 | Dwellir: 'wss://picasso-rpc.dwellir.com' 354 | } 355 | }, 356 | { 357 | info: 'pichiu', 358 | homepage: 'https://kylin.network/', 359 | paraId: 2102, 360 | text: 'Pichiu', 361 | providers: { 362 | 'Kylin Network': 'wss://kusama.kylin-node.co.uk' 363 | } 364 | }, 365 | { 366 | info: 'polkasmith', 367 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/6595 368 | homepage: 'https://polkasmith.polkafoundry.com/', 369 | paraId: 2009, 370 | text: 'PolkaSmith by PolkaFoundry', 371 | providers: { 372 | PolkaSmith: 'wss://wss-polkasmith.polkafoundry.com' 373 | } 374 | }, 375 | { 376 | info: 'quartz', 377 | homepage: 'https://unique.network/', 378 | paraId: 2095, 379 | text: 'QUARTZ by UNIQUE', 380 | providers: { 381 | OnFinality: 'wss://quartz.api.onfinality.io/public-ws', 382 | 'Unique America': 'wss://us-ws-quartz.unique.network', 383 | 'Unique Asia': 'wss://asia-ws-quartz.unique.network', 384 | 'Unique Europe': 'wss://eu-ws-quartz.unique.network' 385 | } 386 | }, 387 | { 388 | info: 'robonomics', 389 | homepage: 'http://robonomics.network/', 390 | paraId: 2048, 391 | text: 'Robonomics', 392 | providers: { 393 | Airalab: 'wss://kusama.rpc.robonomics.network/', 394 | OnFinality: 'wss://robonomics.api.onfinality.io/public-ws' 395 | } 396 | }, 397 | { 398 | info: 'sakura', 399 | homepage: 'https://clover.finance/', 400 | isUnreachable: true, 401 | paraId: 2016, 402 | text: 'Sakura', 403 | providers: { 404 | Clover: 'wss://api-sakura.clover.finance' 405 | } 406 | }, 407 | { 408 | info: 'shiden', 409 | homepage: 'https://shiden.astar.network/', 410 | paraId: 2007, 411 | text: 'Shiden', 412 | providers: { 413 | StakeTechnologies: 'wss://rpc.shiden.astar.network', 414 | OnFinality: 'wss://shiden.api.onfinality.io/public-ws', 415 | Pinknode: 'wss://public-rpc.pinknode.io/shiden', 416 | Dwellir: 'wss://shiden-rpc.dwellir.com' 417 | } 418 | }, 419 | { 420 | info: 'shiden', 421 | homepage: 'https://shiden.astar.network/', 422 | paraId: 2120, 423 | text: 'Shiden Crowdloan 2', 424 | isUnreachable: true, 425 | providers: { 426 | StakeTechnologies: 'wss://rpc.shiden.astar.network' 427 | } 428 | }, 429 | { 430 | info: 'sora_ksm', 431 | homepage: 'https://sora.org/', 432 | paraId: 2011, 433 | text: 'SORA Kusama Parachain', 434 | providers: { 435 | Soramitsu: 'wss://ws.parachain-collator-1.c1.sora2.soramitsu.co.jp' 436 | } 437 | }, 438 | { 439 | info: 'subgame', 440 | homepage: 'http://subgame.org/', 441 | paraId: 2018, 442 | text: 'SubGame Gamma', 443 | providers: { 444 | SubGame: 'wss://gamma.subgame.org/' 445 | } 446 | }, 447 | { 448 | info: 'subsocialX', 449 | homepage: 'https://subsocial.network/', 450 | paraId: 2100, 451 | text: 'SubsocialX', 452 | providers: { 453 | Dappforce: 'wss://para.subsocial.network' 454 | } 455 | }, 456 | { 457 | info: 'tanganika', 458 | homepage: 'https://www.datahighway.com/', 459 | paraId: 2116, 460 | text: 'Tanganika', 461 | providers: { 462 | DataHighway: 'wss://tanganika.datahighway.com' 463 | } 464 | }, 465 | { 466 | info: 'trustbase', 467 | isUnreachable: true, // no providers (yet) 468 | homepage: 'https://trustbase.network/', 469 | paraId: 2078, 470 | text: 'TrustBase', 471 | providers: {} 472 | }, 473 | { 474 | info: 'turing', 475 | homepage: 'https://oak.tech', 476 | paraId: 2114, 477 | text: 'Turing Network', 478 | providers: { 479 | OAK: 'wss://rpc.turing.oak.tech', 480 | OnFinality: 'wss://turing.api.onfinality.io/public-ws', 481 | Dwellir: 'wss://turing-rpc.dwellir.com' 482 | } 483 | }, 484 | { 485 | info: 'unorthodox', 486 | homepage: 'https://standard.tech/', 487 | paraId: 2094, 488 | text: 'Unorthodox', 489 | providers: { 490 | 'Standard Protocol': 'wss://rpc.kusama.standard.tech' 491 | } 492 | }, 493 | { 494 | info: 'zeitgeist', 495 | homepage: 'https://zeitgeist.pm', 496 | paraId: 2101, 497 | text: 'Zeitgeist', 498 | providers: { 499 | ZeitgeistPM: 'wss://rpc-0.zeitgeist.pm', 500 | Dwellir: 'wss://zeitgeist-rpc.dwellir.com', 501 | OnFinality: 'wss://zeitgeist.api.onfinality.io/public-ws' 502 | } 503 | } 504 | ]; 505 | 506 | export const prodParasKusamaCommon: EndpointOption[] = [ 507 | { 508 | info: 'statemine', 509 | paraId: 1000, 510 | text: 'Statemine', 511 | providers: { 512 | Parity: 'wss://statemine-rpc.polkadot.io', 513 | OnFinality: 'wss://statemine.api.onfinality.io/public-ws', 514 | Dwellir: 'wss://statemine-rpc.dwellir.com', 515 | Pinknode: 'wss://public-rpc.pinknode.io/statemine' 516 | }, 517 | teleport: [-1] 518 | }, 519 | { 520 | info: 'encointer', 521 | homepage: 'https://encointer.org/', 522 | paraId: 1001, 523 | text: 'Encointer Network', 524 | providers: { 525 | 'Encointer Association': 'wss://kusama.api.encointer.org', 526 | OnFinality: 'wss://encointer.api.onfinality.io/public-ws' 527 | }, 528 | teleport: [-1] 529 | } 530 | ]; 531 | 532 | export const prodRelayKusama: EndpointOption = { 533 | dnslink: 'kusama', 534 | info: 'kusama', 535 | text: 'Kusama', 536 | providers: { 537 | Parity: 'wss://kusama-rpc.polkadot.io', 538 | OnFinality: 'wss://kusama.api.onfinality.io/public-ws', 539 | Dwellir: 'wss://kusama-rpc.dwellir.com', 540 | RadiumBlock: 'wss://kusama.public.curie.radiumblock.xyz/ws', 541 | Pinknode: 'wss://public-rpc.pinknode.io/kusama', 542 | // 'Geometry Labs': 'wss://kusama.geometry.io/websockets', // https://github.com/polkadot-js/apps/pull/6746 543 | 'light client': 'light://substrate-connect/kusama' 544 | }, 545 | teleport: [1000, 1001], 546 | linked: [...prodParasKusamaCommon, ...prodParasKusama] 547 | }; 548 | 549 | export const prodParasPolkadot: EndpointOption[] = [ 550 | { 551 | info: 'acala', 552 | homepage: 'https://acala.network/', 553 | paraId: 2000, 554 | text: 'Acala', 555 | providers: { 556 | 'Acala Foundation 0': 'wss://acala-rpc-0.aca-api.network', 557 | 'Acala Foundation 1': 'wss://acala-rpc-1.aca-api.network', 558 | // 'Acala Foundation 2': 'wss://acala-rpc-2.aca-api.network/ws', // https://github.com/polkadot-js/apps/issues/6965 559 | 'Acala Foundation 3': 'wss://acala-rpc-3.aca-api.network/ws', 560 | 'Polkawallet 0': 'wss://acala.polkawallet.io', 561 | OnFinality: 'wss://acala-polkadot.api.onfinality.io/public-ws', 562 | Dwellir: 'wss://acala-rpc.dwellir.com' 563 | } 564 | }, 565 | { 566 | info: 'odyssey', 567 | homepage: 'https://www.aresprotocol.io/', 568 | paraId: 2028, 569 | text: 'Ares Odyssey', 570 | providers: { 571 | AresProtocol: 'wss://wss.odyssey.aresprotocol.io' 572 | } 573 | }, 574 | { 575 | info: 'astar', 576 | homepage: 'https://astar.network', 577 | paraId: 2006, 578 | text: 'Astar', 579 | providers: { 580 | Astar: 'wss://rpc.astar.network', 581 | OnFinality: 'wss://astar.api.onfinality.io/public-ws', 582 | Dwellir: 'wss://astar-rpc.dwellir.com', 583 | Pinknode: 'wss://public-rpc.pinknode.io/astar' 584 | } 585 | }, 586 | { 587 | info: 'bifrost', 588 | homepage: 'https://crowdloan.bifrost.app', 589 | paraId: 2030, 590 | text: 'Bifrost', 591 | providers: { 592 | Liebi: 'wss://hk.p.bifrost-rpc.liebi.com/ws' 593 | } 594 | }, 595 | { 596 | info: 'centrifuge', 597 | homepage: 'https://centrifuge.io', 598 | paraId: 2031, 599 | text: 'Centrifuge', 600 | providers: { 601 | Centrifuge: 'wss://fullnode.parachain.centrifuge.io', 602 | OnFinality: 'wss://centrifuge-parachain.api.onfinality.io/public-ws' 603 | } 604 | }, 605 | { 606 | info: 'clover', 607 | homepage: 'https://clover.finance', 608 | paraId: 2002, 609 | text: 'Clover', 610 | providers: { 611 | Clover: 'wss://rpc-para.clover.finance', 612 | OnFinality: 'wss://clover.api.onfinality.io/public-ws' 613 | } 614 | }, 615 | { 616 | // this is also a duplicate as a Live and Testing network - 617 | // it is either/or, not and 618 | info: 'coinversation', 619 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/6635 620 | homepage: 'http://www.coinversation.io/', 621 | paraId: 2027, 622 | text: 'Coinversation', 623 | providers: { 624 | Coinversation: 'wss://rpc.coinversation.io/' 625 | } 626 | }, 627 | { 628 | info: 'composableFinance', 629 | homepage: 'https://composable.finance/', 630 | paraId: 2019, 631 | text: 'Composable Finance', 632 | providers: { 633 | Composable: 'wss://rpc.composable.finance', 634 | Dwellir: 'wss://composable-rpc.dwellir.com' 635 | } 636 | }, 637 | { 638 | info: 'crustParachain', 639 | homepage: 'https://crust.network', 640 | paraId: 2008, 641 | isUnreachable: true, 642 | text: 'Crust', 643 | providers: { 644 | Crust: 'wss://rpc.crust.network' 645 | } 646 | }, 647 | { 648 | info: 'darwinia', 649 | homepage: 'https://darwinia.network/', 650 | paraId: 2046, 651 | text: 'Darwinia', 652 | providers: { 653 | Darwinia: 'wss://parachain-rpc.darwinia.network' 654 | } 655 | }, 656 | { 657 | info: 'darwinia', 658 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/6530 659 | homepage: 'https://darwinia.network/', 660 | paraId: 2003, 661 | text: 'Darwinia Para Backup', 662 | providers: { 663 | Darwinia: 'wss://parachain-rpc.darwinia.network' 664 | } 665 | }, 666 | { 667 | info: 'efinity', 668 | homepage: 'https://efinity.io', 669 | paraId: 2021, 670 | text: 'Efinity', 671 | providers: { 672 | Efinity: 'wss://rpc.efinity.io' 673 | } 674 | }, 675 | { 676 | info: 'equilibrium', 677 | homepage: 'https://equilibrium.io/', 678 | paraId: 2011, 679 | text: 'Equilibrium', 680 | providers: { 681 | Equilibrium: 'wss://node.pol.equilibrium.io/' 682 | } 683 | }, 684 | { 685 | info: 'geminis', 686 | isUnreachable: true, 687 | homepage: 'https://geminis.network/', 688 | paraId: 2038, 689 | text: 'Geminis', 690 | providers: { 691 | Geminis: 'wss://rpc.geminis.network' 692 | } 693 | }, 694 | { 695 | info: 'hydra', 696 | homepage: 'https://hydradx.io/', 697 | paraId: 2034, 698 | text: 'HydraDX', 699 | providers: { 700 | 'Galactic Council': 'wss://rpc-01.hydradx.io', 701 | Dwellir: 'wss://hydradx-rpc.dwellir.com' 702 | } 703 | }, 704 | { 705 | info: 'integritee', 706 | homepage: 'https://integritee.network', 707 | paraId: 2039, 708 | text: 'Integritee Shell', 709 | providers: { 710 | Integritee: 'wss://polkadot.api.integritee.network' 711 | } 712 | }, 713 | { 714 | info: 'interlay', 715 | homepage: 'https://interlay.io/', 716 | paraId: 2032, 717 | text: 'Interlay', 718 | providers: { 719 | 'Kintsugi Labs': 'wss://api.interlay.io/parachain', 720 | OnFinality: 'wss://interlay.api.onfinality.io/public-ws' 721 | } 722 | }, 723 | { 724 | info: 'kapex', 725 | homepage: 'https://totemaccounting.com/', 726 | paraId: 2007, 727 | text: 'Kapex', 728 | providers: { 729 | Totem: 'wss://k-ui.kapex.network' 730 | } 731 | }, 732 | { 733 | info: 'kylin', 734 | homepage: 'https://kylin.network/', 735 | paraId: 2052, 736 | text: 'Kylin', 737 | providers: { 738 | 'Kylin Network': 'wss://polkadot.kylin-node.co.uk' 739 | } 740 | }, 741 | { 742 | info: 'litentry', 743 | homepage: 'https://crowdloan.litentry.com', 744 | paraId: 2013, 745 | text: 'Litentry', 746 | providers: { 747 | Litentry: 'wss://rpc.litentry-parachain.litentry.io', 748 | Dwellir: 'wss://litentry-rpc.dwellir.com' 749 | } 750 | }, 751 | { 752 | info: 'manta', 753 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/7018 754 | homepage: 'https://manta.network', 755 | paraId: 2015, 756 | text: 'Manta', 757 | providers: { 758 | // 'Manta Kuhlii': 'wss://kuhlii.manta.systems', // https://github.com/polkadot-js/apps/issues/6930 759 | // 'Manta Munkiana': 'wss://munkiana.manta.systems', // https://github.com/polkadot-js/apps/issues/6871 760 | // 'Manta Pectinata': 'wss://pectinata.manta.systems' // https://github.com/polkadot-js/apps/issues/7018 761 | } 762 | }, 763 | { 764 | info: 'moonbeam', 765 | homepage: 'https://moonbeam.network/networks/moonbeam/', 766 | paraId: 2004, 767 | text: 'Moonbeam', 768 | providers: { 769 | 'Moonbeam Foundation': 'wss://wss.api.moonbeam.network', 770 | Blast: 'wss://moonbeam.public.blastapi.io', 771 | Dwellir: 'wss://moonbeam-rpc.dwellir.com', 772 | OnFinality: 'wss://moonbeam.api.onfinality.io/public-ws', 773 | Pinknode: 'wss://public-rpc.pinknode.io/moonbeam' 774 | } 775 | }, 776 | { 777 | info: 'nodle', 778 | homepage: 'https://nodle.com', 779 | paraId: 2026, 780 | text: 'Nodle', 781 | providers: { 782 | OnFinality: 'wss://nodle-parachain.api.onfinality.io/public-ws', 783 | Dwellir: 'wss://eden-rpc.dwellir.com', 784 | Pinknode: 'wss://public-rpc.pinknode.io/nodle' 785 | } 786 | }, 787 | { 788 | info: 'omnibtc', 789 | isUnreachable: true, 790 | homepage: 'https://www.omnibtc.finance', 791 | text: 'OmniBTC', 792 | paraId: 2053, 793 | providers: { 794 | OmniBTC: 'wss://omnibtc.io/ws' 795 | } 796 | }, 797 | { 798 | info: 'origintrail-parachain', 799 | homepage: 'https://parachain.origintrail.io', 800 | text: 'OriginTrail Parachain', 801 | paraId: 2043, 802 | providers: { 803 | TraceLabs: 'wss://parachain-rpc.origin-trail.network' 804 | } 805 | }, 806 | { 807 | info: 'parallel', 808 | homepage: 'https://parallel.fi', 809 | paraId: 2012, 810 | text: 'Parallel', 811 | providers: { 812 | OnFinality: 'wss://parallel.api.onfinality.io/public-ws', 813 | Parallel: 'wss://rpc.parallel.fi' 814 | } 815 | }, 816 | { 817 | info: 'phala', 818 | homepage: 'https://phala.network', 819 | paraId: 2035, 820 | text: 'Phala Network', 821 | providers: { 822 | Phala: 'wss://api.phala.network/ws' 823 | } 824 | }, 825 | { 826 | info: 'polkadex', 827 | isUnreachable: true, // https://github.com/polkadot-js/apps/issues/7620 828 | homepage: 'https://polkadex.trade/', 829 | paraId: 2040, 830 | text: 'Polkadex', 831 | providers: { 832 | // 'Polkadex Team': 'wss://mainnet.polkadex.trade/', // https://github.com/polkadot-js/apps/issues/7620 833 | // OnFinality: 'wss://polkadex.api.onfinality.io/public-ws' // https://github.com/polkadot-js/apps/issues/7620 834 | } 835 | }, 836 | { 837 | info: 'subdao', 838 | homepage: 'https://subdao.network/', 839 | paraId: 2018, 840 | isUnreachable: true, 841 | text: 'SubDAO', 842 | providers: { 843 | SubDAO: 'wss://parachain-rpc.subdao.org' 844 | } 845 | }, 846 | { 847 | info: 'subgame', 848 | homepage: 'http://subgame.org/', 849 | isUnreachable: true, // https://github.com/polkadot-js/apps/pull/6761 850 | paraId: 2017, 851 | text: 'SubGame Gamma', 852 | providers: { 853 | SubGame: 'wss://gamma.subgame.org/' 854 | } 855 | }, 856 | { 857 | info: 'unique', 858 | homepage: 'https://unique.network/', 859 | paraId: 2037, 860 | text: 'Unique Network', 861 | providers: { 862 | 'Unique America': 'wss://us-ws.unique.network/', 863 | 'Unique Asia': 'wss://asia-ws.unique.network/', 864 | 'Unique Europe': 'wss://eu-ws.unique.network/' 865 | } 866 | } 867 | ]; 868 | 869 | export const prodParasPolkadotCommon: EndpointOption[] = [ 870 | { 871 | info: 'statemint', 872 | paraId: 1000, 873 | text: 'Statemint', 874 | teleport: [-1], 875 | providers: { 876 | Parity: 'wss://statemint-rpc.polkadot.io', 877 | OnFinality: 'wss://statemint.api.onfinality.io/public-ws', 878 | Dwellir: 'wss://statemint-rpc.dwellir.com', 879 | Pinknode: 'wss://public-rpc.pinknode.io/statemint' 880 | } 881 | } 882 | ]; 883 | 884 | export const prodRelayPolkadot: EndpointOption = { 885 | dnslink: 'polkadot', 886 | info: 'polkadot', 887 | text: 'Polkadot', 888 | providers: { 889 | Parity: 'wss://rpc.polkadot.io', 890 | OnFinality: 'wss://polkadot.api.onfinality.io/public-ws', 891 | Dwellir: 'wss://polkadot-rpc.dwellir.com', 892 | Pinknode: 'wss://public-rpc.pinknode.io/polkadot', 893 | RadiumBlock: 'wss://polkadot.public.curie.radiumblock.io/ws', 894 | // 'Geometry Labs': 'wss://polkadot.geometry.io/websockets', // https://github.com/polkadot-js/apps/pull/6746 895 | 'light client': 'light://substrate-connect/polkadot' 896 | }, 897 | teleport: [1000], 898 | linked: [...prodParasPolkadotCommon, ...prodParasPolkadot] 899 | }; 900 | 901 | export const prodChains: EndpointOption[] = [ 902 | { 903 | info: 'aleph', 904 | text: 'Aleph Zero', 905 | providers: { 906 | 'Aleph Zero Foundation': 'wss://ws.azero.dev' 907 | } 908 | }, 909 | { 910 | info: 'Ares Odyssey', 911 | text: 'Ares Odyssey', 912 | providers: { 913 | 'Ares Protocol': 'wss://odyssey.aresprotocol.io' 914 | } 915 | }, 916 | { 917 | info: 'automata', 918 | text: 'Automata', 919 | providers: { 920 | 'Automata Network': 'wss://api.ata.network', 921 | OnFinality: 'wss://automata.api.onfinality.io/public-ws' 922 | } 923 | }, 924 | { 925 | dnslink: 'centrifuge', 926 | info: 'centrifuge', 927 | text: 'Centrifuge Standalone [Archived]', 928 | providers: { 929 | Centrifuge: 'wss://fullnode.centrifuge.io' 930 | } 931 | }, 932 | { 933 | info: 'chainx', 934 | text: 'ChainX', 935 | providers: { 936 | ChainX: 'wss://mainnet.chainx.org/ws' 937 | } 938 | }, 939 | { 940 | info: 'competitors-club', 941 | text: 'Competitors Club', 942 | providers: { 943 | 'Competitors Club': 'wss://node0.competitors.club/wss' 944 | } 945 | }, 946 | { 947 | info: 'creditcoin', 948 | text: 'Creditcoin', 949 | providers: { 950 | 'Creditcoin Foundation': 'wss://mainnet.creditcoin.network' 951 | } 952 | }, 953 | { 954 | info: 'crown-sterling', 955 | text: 'Crown Sterling', 956 | providers: { 957 | 'Crown Sterling': 'wss://blockchain.crownsterling.io' 958 | } 959 | }, 960 | { 961 | info: 'crust', 962 | text: 'Crust Network', 963 | providers: { 964 | 'Crust Network': 'wss://rpc.crust.network', 965 | OnFinality: 'wss://crust.api.onfinality.io/public-ws' 966 | } 967 | }, 968 | { 969 | info: 'darwinia', 970 | text: 'Darwinia', 971 | providers: { 972 | 'Darwinia Network': 'wss://rpc.darwinia.network', 973 | Dwellir: 'wss://darwinia-rpc.dwellir.com' 974 | } 975 | }, 976 | { 977 | info: 'crab', 978 | text: 'Darwinia Crab', 979 | providers: { 980 | 'Darwinia Network': 'wss://crab-rpc.darwinia.network', 981 | Dwellir: 'wss://darwiniacrab-rpc.dwellir.com', 982 | OnFinality: 'wss://darwinia-crab.api.onfinality.io/public-ws' 983 | } 984 | }, 985 | { 986 | info: 'dock-pos-mainnet', 987 | text: 'Dock', 988 | providers: { 989 | 'Dock Association': 'wss://mainnet-node.dock.io' 990 | } 991 | }, 992 | { 993 | dnslink: 'edgeware', 994 | info: 'edgeware', 995 | text: 'Edgeware', 996 | providers: { 997 | 'Commonwealth Labs': 'wss://mainnet.edgewa.re', 998 | OnFinality: 'wss://edgeware.api.onfinality.io/public-ws', 999 | Dwellir: 'wss://edgeware-rpc.dwellir.com' 1000 | } 1001 | }, 1002 | { 1003 | info: 'efinity', 1004 | isDisabled: true, // https://github.com/polkadot-js/apps/pull/6761 1005 | text: 'Efinity', 1006 | providers: { 1007 | Efinity: 'wss://rpc.efinity.io' 1008 | } 1009 | }, 1010 | { 1011 | info: 'equilibrium', 1012 | isDisabled: true, // https://github.com/polkadot-js/apps/issues/7219 1013 | text: 'Equilibrium', 1014 | providers: { 1015 | Equilibrium: 'wss://node.equilibrium.io' 1016 | } 1017 | }, 1018 | { 1019 | info: 'genshiro', 1020 | text: 'Genshiro', 1021 | providers: { 1022 | Equilibrium: 'wss://node.genshiro.io' 1023 | } 1024 | }, 1025 | { 1026 | info: 'hanonycash', 1027 | isDisabled: true, // https://github.com/polkadot-js/apps/runs/2755409009?check_suite_focus=true 1028 | text: 'Hanonycash', 1029 | providers: { 1030 | Hanonycash: 'wss://rpc.hanonycash.com' 1031 | } 1032 | }, 1033 | { 1034 | dnslink: 'kulupu', 1035 | info: 'kulupu', 1036 | text: 'Kulupu', 1037 | providers: { 1038 | Kulupu: 'wss://rpc.kulupu.corepaper.org/ws' 1039 | } 1040 | }, 1041 | { 1042 | info: 'kusari', 1043 | text: 'Kusari', 1044 | providers: { 1045 | Swapdex: 'wss://ws.kusari.network' 1046 | } 1047 | }, 1048 | { 1049 | info: 'logion', 1050 | text: 'logion Standalone', 1051 | providers: { 1052 | Logion: 'wss://rpc01.logion.network' 1053 | } 1054 | }, 1055 | { 1056 | info: 'mathchain', 1057 | text: 'MathChain', 1058 | providers: { 1059 | MathWallet: 'wss://mathchain-asia.maiziqianbao.net/ws', 1060 | 'MathWallet Backup': 'wss://mathchain-us.maiziqianbao.net/ws' 1061 | } 1062 | }, 1063 | { 1064 | info: 'minix', 1065 | isDisabled: true, // https://github.com/polkadot-js/apps/issues/7182 1066 | text: 'MiniX', 1067 | providers: { 1068 | ChainX: 'wss://minichain-mainnet.coming.chat/ws' 1069 | } 1070 | }, 1071 | { 1072 | info: 'myriad', 1073 | text: 'Myriad', 1074 | providers: { 1075 | Myriad: 'wss://ws-rpc.myriad.social' 1076 | } 1077 | }, 1078 | { 1079 | info: 'neatcoin', 1080 | text: 'Neatcoin', 1081 | providers: { 1082 | Neatcoin: 'wss://rpc.neatcoin.org/ws' 1083 | } 1084 | }, 1085 | { 1086 | info: 'nftmart', 1087 | text: 'NFTMart', 1088 | providers: { 1089 | NFTMart: 'wss://mainnet.nftmart.io/rpc/ws' 1090 | } 1091 | }, 1092 | { 1093 | info: 'nodle', 1094 | text: 'Nodle', 1095 | providers: { 1096 | // Nodle: 'wss://main3.nodleprotocol.io', // https://github.com/polkadot-js/apps/issues/7652 1097 | OnFinality: 'wss://nodle.api.onfinality.io/public-ws' 1098 | } 1099 | }, 1100 | { 1101 | info: 'polkadex', 1102 | text: 'Polkadex', 1103 | providers: { 1104 | 'Polkadex Team': 'wss://mainnet.polkadex.trade', 1105 | OnFinality: 'wss://polkadex.api.onfinality.io/public-ws' 1106 | } 1107 | }, 1108 | { 1109 | info: 'polymesh', 1110 | text: 'Polymesh Mainnet', 1111 | providers: { 1112 | Polymath: 'wss://mainnet-rpc.polymesh.network' 1113 | } 1114 | }, 1115 | { 1116 | info: 'riochain', 1117 | text: 'RioChain', 1118 | providers: { 1119 | RioChain: 'wss://node.v1.riochain.io' 1120 | } 1121 | }, 1122 | { 1123 | info: 'robonomics', 1124 | isDisabled: true, // https://github.com/polkadot-js/apps/pull/6761 1125 | text: 'Robonomics', 1126 | providers: { 1127 | Airalab: 'wss://kusama.rpc.robonomics.network/' 1128 | } 1129 | }, 1130 | { 1131 | info: 'sherpax', 1132 | text: 'SherpaX', 1133 | providers: { 1134 | ChainX: 'wss://mainnet.sherpax.io' 1135 | } 1136 | }, 1137 | { 1138 | info: 'sora-substrate', 1139 | text: 'SORA', 1140 | providers: { 1141 | 'SORA Parliament Ministry of Finance #2': 'wss://mof2.sora.org', 1142 | 'SORA Parliament Ministry of Finance': 'wss://ws.mof.sora.org', 1143 | 'SORA Parliament Ministry of Finance #3': 'wss://mof3.sora.org', 1144 | // Soramitsu: 'wss://ws.alb.sora.org', // https://github.com/polkadot-js/apps/issues/7786 1145 | OnFinality: 'wss://sora.api.onfinality.io/public-ws' 1146 | // 'SORA Community (Lux8)': 'wss://sora.lux8.net' // https://github.com/polkadot-js/apps/issues/6195 1147 | } 1148 | }, 1149 | { 1150 | info: 'spanner', 1151 | isDisabled: true, // https://github.com/polkadot-js/apps/issues/6547 1152 | text: 'Spanner', 1153 | providers: { 1154 | Spanner: 'wss://wss.spannerprotocol.com' 1155 | } 1156 | }, 1157 | { 1158 | info: 'stafi', 1159 | isDisabled: true, // Cannot find type ChainId 1160 | text: 'Stafi', 1161 | providers: { 1162 | 'Stafi Foundation': 'wss://mainnet-rpc.stafi.io' 1163 | } 1164 | }, 1165 | { 1166 | info: 'subgame', 1167 | text: 'SubGame', 1168 | providers: { 1169 | SubGame: 'wss://mainnet.subgame.org/' 1170 | } 1171 | }, 1172 | { 1173 | info: 'subsocial', 1174 | text: 'Subsocial', 1175 | providers: { 1176 | DappForce: 'wss://rpc.subsocial.network' 1177 | } 1178 | }, 1179 | { 1180 | info: 'swapdex', 1181 | text: 'Swapdex', 1182 | providers: { 1183 | Swapdex: 'wss://ws.swapdex.network' 1184 | } 1185 | }, 1186 | { 1187 | info: 'ternoa', 1188 | text: 'Ternoa', 1189 | providers: { 1190 | CapsuleCorp: 'wss://mainnet.ternoa.network' 1191 | } 1192 | }, 1193 | { 1194 | info: 'uniarts', 1195 | text: 'UniArts', 1196 | providers: { 1197 | UniArts: 'wss://mainnet.uniarts.vip:9443' 1198 | } 1199 | }, 1200 | { 1201 | info: 'westlake', 1202 | isDisabled: true, // https://github.com/polkadot-js/apps/issues/7293 1203 | text: 'Westlake', 1204 | providers: { 1205 | DataHighway: 'wss://westlake.datahighway.com' 1206 | } 1207 | } 1208 | ]; 1209 | -------------------------------------------------------------------------------- /core/src/endpoints.ts: -------------------------------------------------------------------------------- 1 | import { 2 | prodChains, 3 | prodParasKusama, 4 | prodParasPolkadot, 5 | prodRelayKusama, 6 | prodRelayPolkadot 7 | } from './endpoint_dump'; 8 | 9 | export enum Ecosystem { 10 | Polkadot = 'Polkadot', 11 | Kusama = 'Kusama', 12 | None = 'None' 13 | } 14 | 15 | export interface IChainEndpoint { 16 | name: string; 17 | ecosystem: Ecosystem; 18 | endpoints: Record; 19 | } 20 | 21 | export const allChains: IChainEndpoint[] = []; 22 | const addChain = (e: any, ecosystem: Ecosystem) => 23 | allChains.push({ name: e.text, ecosystem, endpoints: e.providers }); 24 | 25 | // Polkadot stuff 26 | addChain(prodRelayPolkadot, Ecosystem.Polkadot); 27 | prodRelayPolkadot.linked?.forEach((e) => addChain(e, Ecosystem.Polkadot)); 28 | 29 | // Kusama stuff 30 | addChain(prodRelayKusama, Ecosystem.Kusama); 31 | prodRelayKusama.linked?.forEach((e) => addChain(e, Ecosystem.Kusama)); 32 | 33 | // everything else. 34 | prodChains.forEach((e) => addChain(e, Ecosystem.None)); 35 | 36 | export function paraIdToName(id: number, ecosystem: Ecosystem): string | undefined { 37 | if (ecosystem == Ecosystem.Kusama) { 38 | const maybeChain = prodParasKusama.find((c) => { 39 | if (c.paraId && c.paraId === id) { 40 | return true; 41 | } 42 | }); 43 | return maybeChain ? maybeChain.text : undefined; 44 | } else if (ecosystem == Ecosystem.Polkadot) { 45 | const maybeChain = prodParasPolkadot.find((c) => { 46 | if (c.paraId && c.paraId === id) { 47 | return true; 48 | } 49 | }); 50 | return maybeChain ? maybeChain.text : undefined; 51 | } else { 52 | return undefined; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/fetch/acala.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise } from '@polkadot/api'; 2 | import { Asset, AssetOrigin } from '../types'; 3 | import { OrmlAccountData } from '@open-web3/orml-types/interfaces/'; 4 | import { types } from '@acala-network/types'; 5 | import BN from 'bn.js'; 6 | import { findDecimals, tickerPrice } from '../utils'; 7 | import { IChain, IValueBearing } from '.'; 8 | import { Account32ValueBearing } from './substrate'; 9 | import { 10 | AcalaPrimitivesCurrencyCurrencyId, 11 | AcalaPrimitivesCurrencyDexShare, 12 | ModuleIncentivesPoolId 13 | } from '@acala-network/types/interfaces/types-lookup'; 14 | 15 | type CurrencyId = AcalaPrimitivesCurrencyCurrencyId; 16 | type PoolId = ModuleIncentivesPoolId; 17 | 18 | async function findPriceOrCheckDex(api: ApiPromise, x: CurrencyId, y: CurrencyId): Promise { 19 | const normal = await tickerPrice(formatCurrencyId(x)); 20 | if (normal > 0) { 21 | return normal; 22 | } else { 23 | return await findPriceViaDex(api, x, y); 24 | } 25 | } 26 | 27 | const PRICE_CACHE: Map = new Map(); 28 | 29 | // find the price of an asset by looking into its dex value, when converted into another one. This 30 | // is useful to fund the price of something like LKSM, LC-DOT or LDOT. It takes `amount` of 31 | // `unknown` and returns 32 | async function findPriceViaDex(api: ApiPromise, x: CurrencyId, y: CurrencyId): Promise { 33 | if (PRICE_CACHE.has(formatCurrencyId(x))) { 34 | return PRICE_CACHE.get(formatCurrencyId(x))!; 35 | } 36 | 37 | const [xTotal, yTotal] = await dexLiquidityOf(api, x, y); 38 | if (xTotal.isZero() || yTotal.isZero()) { 39 | console.log( 40 | `⛔️ failed to get price of ${x.toString()} via ${y.toString()}. Does a pool for them exist?` 41 | ); 42 | return 0; 43 | } 44 | 45 | // now get the price of `y`, then known token.. 46 | const MUL = 10000; 47 | const yPrice = new BN((await tickerPrice(formatCurrencyId(y))) * MUL); 48 | console.log( 49 | `🎩 price of ${x.toString()} via ${y.toString()} is ${yTotal.mul(yPrice).div(xTotal).toNumber() / MUL 50 | }` 51 | ); 52 | const price = yTotal.mul(yPrice).div(xTotal).toNumber() / MUL; 53 | PRICE_CACHE.set(formatCurrencyId(x), price); 54 | return price; 55 | } 56 | 57 | async function dexLiquidityOf(api: ApiPromise, x: CurrencyId, y: CurrencyId): Promise<[BN, BN]> { 58 | let [yTotal, xTotal]: [BN, BN] = [new BN(0), new BN(0)]; 59 | try { 60 | //@ts-ignore 61 | [yTotal, xTotal] = await api.query.dex.liquidityPool([y.toHuman(), x.toHuman()]); 62 | } catch { 63 | //@ts-ignore 64 | [xTotal, yTotal] = await api.query.dex.liquidityPool([x.toHuman(), y.toHuman()]); 65 | } 66 | return [xTotal, yTotal]; 67 | } 68 | 69 | function formatCurrencyId(currencyId: CurrencyId): string { 70 | if (currencyId.isDexShare) { 71 | const p0 = currencyId.asDexShare[0] as CurrencyId; 72 | const p1 = currencyId.asDexShare[1] as CurrencyId; 73 | return `LP ${formatCurrencyId(p0)}-${formatCurrencyId(p1)}`; 74 | } else if (currencyId.isForeignAsset) { 75 | return `foreignAsset${currencyId.asForeignAsset}`; 76 | } else if (currencyId.isLiquidCrowdloan) { 77 | return `LC-${currencyId.asLiquidCrowdloan}`; 78 | } else if (currencyId.isToken) { 79 | return `${currencyId.asToken.toString()}`; 80 | } else { 81 | return 'UNKNOWN_CURRENCY'; 82 | } 83 | } 84 | 85 | function formatDexShare(share: AcalaPrimitivesCurrencyDexShare): string { 86 | if (share.isToken) { 87 | return share.asToken.toString(); 88 | } else if (share.isErc20) { 89 | return `ERC20-${share.asErc20}`; 90 | } else { 91 | return `UNKNOWN_DEX_SHARE`; 92 | } 93 | } 94 | 95 | function poolToTokenName(pool: PoolId): string { 96 | if (pool.isDex) { 97 | return formatCurrencyId(pool.asDex); 98 | } else if (pool.isLoans) { 99 | return `${formatCurrencyId(pool.asLoans)}`; 100 | } else { 101 | return 'UNKNOWN_POOL'; 102 | } 103 | } 104 | 105 | function poolToName(pool: ModuleIncentivesPoolId): string { 106 | if (pool.isDex) { 107 | return formatCurrencyId(pool.asDex); 108 | } else if (pool.isLoans) { 109 | return `Loaned-${formatCurrencyId(pool.asLoans)}`; 110 | } else { 111 | return 'UNKNOWN_POOL'; 112 | } 113 | } 114 | 115 | async function processToken( 116 | api: ApiPromise, 117 | token: CurrencyId, 118 | accountData: OrmlAccountData, 119 | origin: AssetOrigin 120 | ): Promise { 121 | if (token.isToken) { 122 | const tokenName = token.asToken.toString(); 123 | const knownToken = api.createType('CurrencyId', { Token: api.registry.chainTokens[0] }); 124 | // @ts-ignore 125 | const price = await findPriceOrCheckDex(api, token, knownToken); 126 | const decimals = findDecimals(api, tokenName); 127 | return new Asset({ 128 | amount: accountData.free, 129 | decimals, 130 | name: formatCurrencyId(token), 131 | price, 132 | ticker: tokenName, 133 | transferrable: true, 134 | origin 135 | }); 136 | } else if (token.isForeignAsset) { 137 | const assetMetadata = ( 138 | await api.query.assetRegistry.assetMetadatas({ ForeignAssetId: token.asForeignAsset }) 139 | ).unwrap(); 140 | const ticker = assetMetadata.toHuman().name?.toString().toLowerCase() || 'XXX'; 141 | const price = await tickerPrice(ticker); 142 | return new Asset({ 143 | amount: accountData.free, 144 | decimals: new BN(assetMetadata.decimals), 145 | name: formatCurrencyId(token), 146 | price, 147 | ticker, 148 | transferrable: true, 149 | origin 150 | }); 151 | } else { 152 | undefined; 153 | } 154 | } 155 | 156 | export class AcalaLPTokens extends Account32ValueBearing implements IValueBearing { 157 | identifiers: string[]; 158 | 159 | constructor() { 160 | super(); 161 | this.identifiers = ['rewards', 'dex']; 162 | } 163 | 164 | async extract(chain: IChain, account: string): Promise { 165 | const { api, name: chainName } = chain; 166 | api.registerTypes(types); 167 | const assets: Asset[] = []; 168 | 169 | const allPoolIds = (await api.query.rewards.poolInfos.entries()).map( 170 | ([key, value]) => key.args[0] 171 | ); 172 | for (const poolId of allPoolIds) { 173 | const [amount, rewardsMap] = await api.query.rewards.sharesAndWithdrawnRewards( 174 | poolId, 175 | account 176 | ); 177 | 178 | if (!amount.isZero()) { 179 | const poolName = poolToName(poolId); 180 | const poolTokenName = poolToTokenName(poolId); 181 | 182 | // if this is an LP pool, we register each individual asset independently as well. note 183 | // that we also register the LP token, which will always have a value of zero. 184 | if (poolId.isDex && poolId.asDex.isDexShare) { 185 | const p0: CurrencyId = poolId.asDex.asDexShare[0] as CurrencyId; 186 | const p1: CurrencyId = poolId.asDex.asDexShare[1] as CurrencyId; 187 | 188 | //@ts-ignore 189 | const pool = (await api.query.rewards.poolInfos(poolId)) as PoolInfo; 190 | const poolTotalShares = pool.totalShares; 191 | 192 | let poolCurrentShares; 193 | try { 194 | poolCurrentShares = await api.query.dex.liquidityPool([p0.toHuman(), p1.toHuman()]); 195 | } catch { 196 | poolCurrentShares = await api.query.dex.liquidityPool([p1.toHuman(), p0.toHuman()]); 197 | } 198 | //@ts-ignore 199 | const p0Amount = poolCurrentShares[0].mul(amount).div(poolTotalShares); 200 | //@ts-ignore 201 | const p1Amount = poolCurrentShares[1].mul(amount).div(poolTotalShares); 202 | 203 | // wacky, but works: we use the chain's main token as a swappable token in the dex. 204 | const knownToken = api.createType('CurrencyId', { Token: api.registry.chainTokens[0] }); 205 | const p0Name = formatCurrencyId(p0); 206 | const p1Name = formatCurrencyId(p1); 207 | 208 | assets.push( 209 | new Asset({ 210 | amount: p0Amount, 211 | decimals: findDecimals(api, p0Name), 212 | name: `[LP-derived] ${p0Name}`, 213 | ticker: p0Name, 214 | // @ts-ignore 215 | price: await findPriceOrCheckDex(api, p0, knownToken), 216 | transferrable: false, 217 | origin: { account, chain: chainName, source: 'reward pools pallet' } 218 | }) 219 | ); 220 | 221 | assets.push( 222 | new Asset({ 223 | amount: p1Amount, 224 | decimals: findDecimals(api, p1Name), 225 | name: `[LP-derived] ${p1Name}`, 226 | ticker: p1Name, 227 | // @ts-ignore 228 | price: await findPriceOrCheckDex(api, p1, knownToken), 229 | transferrable: false, 230 | origin: { account, chain: chainName, source: 'reward pools pallet' } 231 | }) 232 | ); 233 | } 234 | 235 | assets.push( 236 | new Asset({ 237 | amount, 238 | decimals: findDecimals(api, poolTokenName), 239 | name: poolName, 240 | ticker: poolTokenName, 241 | price: await tickerPrice(poolTokenName), 242 | transferrable: false, 243 | origin: { account, chain: chainName, source: 'reward pools pallet' } 244 | }) 245 | ); 246 | } 247 | } 248 | 249 | return assets; 250 | } 251 | } 252 | 253 | export class AcalaTokens extends Account32ValueBearing implements IValueBearing { 254 | identifiers: string[]; 255 | 256 | constructor() { 257 | super(); 258 | this.identifiers = ['tokens']; 259 | } 260 | 261 | async extract(chain: IChain, account: string): Promise { 262 | const { api, name: chainName } = chain; 263 | api.registerTypes(types); 264 | 265 | const assets: Asset[] = []; 266 | const origin: AssetOrigin = { account, chain: chainName, source: 'tokens pallet' }; 267 | const entries = await api.query.tokens.accounts.entries(account); 268 | 269 | for (const [key, token_data_raw] of entries) { 270 | const token = key.args[1]; 271 | const tokenData = api.createType('OrmlAccountData', token_data_raw); 272 | //@ts-ignore 273 | const maybeAsset = await processToken(api, token, tokenData, origin); 274 | if (maybeAsset) { 275 | assets.push(maybeAsset); 276 | } 277 | } 278 | 279 | return assets; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /core/src/fetch/index.ts: -------------------------------------------------------------------------------- 1 | import '@polkadot/api-augment'; 2 | import { ApiPromise, WsProvider } from '@polkadot/api'; 3 | import { Asset } from '../types'; 4 | import { Assets, System, ParachainCrowdloan } from './substrate'; 5 | import { MoonbeamCrowdloanRewards } from './moonbeam'; 6 | import { tickerPrice } from '../utils'; 7 | import { AcalaLPTokens, AcalaTokens } from './acala'; 8 | 9 | export interface IChain { 10 | api: ApiPromise; 11 | name: string; 12 | } 13 | 14 | export interface IValueBearing { 15 | identifiers: string[]; 16 | addressLength: number; 17 | extract(chain: IChain, account: string): Promise; 18 | } 19 | 20 | export async function makeApi(ws: string): Promise { 21 | const provider = new WsProvider(ws); 22 | const api = await ApiPromise.create({ provider }); 23 | 24 | const name = (await api.rpc.system.chain()).toString(); 25 | // this will cache the price. 26 | const _price = await tickerPrice(api.registry.chainTokens[0]); 27 | 28 | return { api, name }; 29 | } 30 | 31 | export function accountLengthCheck( 32 | api: ApiPromise, 33 | account: string, 34 | expectedLength: number 35 | ): boolean { 36 | try { 37 | return api.createType('AccountId', account).toU8a().length === expectedLength; 38 | } catch { 39 | return false; 40 | } 41 | } 42 | 43 | export async function scrape(account: string, api: ApiPromise): Promise { 44 | let assets: Asset[] = []; 45 | const chain = (await api.rpc.system.chain()).toString(); 46 | const valueBearingModules = [ 47 | new System(), 48 | new Assets(), 49 | new ParachainCrowdloan(), 50 | new MoonbeamCrowdloanRewards(), 51 | new AcalaTokens(), 52 | new AcalaLPTokens() 53 | ]; 54 | 55 | for (const mod of valueBearingModules) { 56 | if (mod.identifiers.length === 0) { 57 | console.log(`❌ module ${JSON.stringify(mod)} has no identifier`); 58 | } 59 | // const instances = api.registry.getModuleInstances(api.runtimeVersion.specName.toString(), mod.identifier); 60 | if (mod.identifiers.every((i) => api.query[i] !== undefined)) { 61 | try { 62 | const moduleAssets = await mod.extract({ api, name: chain }, account); 63 | assets = assets.concat(moduleAssets); 64 | } catch (e) { 65 | // console.warn( 66 | // `error while fetching ${mod.identifiers} for ${account} in chain ${chain}:`, 67 | // e 68 | // ); 69 | // throw(e) 70 | } 71 | } 72 | } 73 | console.log(`✅ fetched ${account} from ${chain}, found ${assets.length} assets.`); 74 | 75 | return assets; 76 | } 77 | -------------------------------------------------------------------------------- /core/src/fetch/moonbeam.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { IChain, IValueBearing } from '.'; 3 | import { Asset } from '../types'; 4 | import { tickerPrice } from '../utils'; 5 | 6 | export class MoonbeamCrowdloanRewards implements IValueBearing { 7 | identifiers: string[]; 8 | addressLength: number; 9 | 10 | constructor() { 11 | this.addressLength = 20; 12 | this.identifiers = ['crowdloanRewards']; 13 | } 14 | 15 | async extract(chain: IChain, account: string): Promise { 16 | const { api, name: chainName } = chain; 17 | const ticker = api.registry.chainTokens[0]; 18 | const price = await tickerPrice(ticker); 19 | // really wacky way of decoding shit... 20 | const [_, total, claimed, _dont_care] = api.createType( 21 | '(U8, Balance, Balance, Vec)', 22 | (await api.query.crowdloanRewards.accountsPayable(account)).toU8a() 23 | ); 24 | const locked = total.sub(claimed); 25 | 26 | if (locked.isZero()) { 27 | return []; 28 | } 29 | 30 | const decimals = new BN(api.registry.chainDecimals[0]); 31 | return [ 32 | new Asset({ 33 | name: `crowdloan vested ${ticker}`, 34 | ticker, 35 | price, 36 | transferrable: false, 37 | amount: locked, 38 | decimals, 39 | origin: { account, chain: chainName, source: 'crowdloan rewards pallet' } 40 | }) 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/fetch/substrate.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { IValueBearing, IChain } from '.'; 3 | import { Ecosystem, paraIdToName } from '../endpoints'; 4 | import { Asset } from '../types'; 5 | import { tickerPrice } from '../utils'; 6 | 7 | export class Account32ValueBearing { 8 | addressLength: number; 9 | 10 | constructor() { 11 | this.addressLength = 32; 12 | } 13 | } 14 | 15 | export class System extends Account32ValueBearing implements IValueBearing { 16 | identifiers: string[]; 17 | 18 | constructor() { 19 | super(); 20 | this.identifiers = ['system']; 21 | } 22 | 23 | async extract(chain: IChain, account: string): Promise { 24 | const ticker = chain.api.registry.chainTokens[0]; 25 | const price = await tickerPrice(ticker); 26 | const accountData = await chain.api.query.system.account(account); 27 | const decimals = new BN(chain.api.registry.chainDecimals[0]); 28 | 29 | const assets: Asset[] = []; 30 | if (!accountData.data.free.isZero()) { 31 | assets.push( 32 | new Asset({ 33 | name: `free_${ticker}`, 34 | ticker, 35 | price, 36 | transferrable: true, 37 | amount: accountData.data.free || new BN(0), 38 | decimals, 39 | origin: { account, chain: chain.name, source: 'system pallet' } 40 | }) 41 | ); 42 | } 43 | if (!accountData.data.reserved.isZero()) { 44 | assets.push( 45 | new Asset({ 46 | name: `reserved_${ticker}`, 47 | ticker, 48 | price, 49 | transferrable: false, 50 | amount: accountData.data.reserved || new BN(0), 51 | decimals, 52 | origin: { account, chain: chain.name, source: 'system pallet' } 53 | }) 54 | ); 55 | } 56 | return assets; 57 | } 58 | } 59 | 60 | export class ParachainCrowdloan extends Account32ValueBearing implements IValueBearing { 61 | identifiers: string[]; 62 | 63 | constructor() { 64 | super(); 65 | this.identifiers = ['paras', 'crowdloan']; 66 | } 67 | 68 | async extract(chain: IChain, account: string): Promise { 69 | const { api, name: chainName } = chain; 70 | // assumption: this pallet only lives on the relay chains. 71 | const ecosystem = chainName.toLowerCase() == 'polkadot' ? Ecosystem.Polkadot : Ecosystem.Kusama; 72 | const ticker = api.registry.chainTokens[0]; 73 | const price = await tickerPrice(ticker); 74 | const accountHex = api.createType('AccountId', account).toHex(); 75 | const decimals = new BN(api.registry.chainDecimals[0]); 76 | const allParaIds: any[] = (await api.query.paras.paraLifecycles.entries()).map( 77 | ([key, _]) => key.args[0] 78 | ); 79 | const assets: Asset[] = []; 80 | const fetchParaIdPromise = allParaIds.map(async (id) => { 81 | const contribution = await api.derive.crowdloan.ownContributions(id, [accountHex]); 82 | if (contribution[accountHex]) { 83 | const contribution_amount = contribution[accountHex]; 84 | if (!contribution_amount.isZero()) { 85 | const asset = new Asset({ 86 | name: `crowdloan_${id}_${paraIdToName(Number(id), ecosystem)}`, 87 | ticker, 88 | price, 89 | transferrable: false, 90 | amount: contribution_amount, 91 | decimals, 92 | origin: { account, chain: chainName, source: 'crowdloan pallet' } 93 | }); 94 | assets.push(asset); 95 | } 96 | } 97 | }); 98 | 99 | await Promise.all(fetchParaIdPromise); 100 | 101 | return assets; 102 | } 103 | } 104 | 105 | export class Assets extends Account32ValueBearing implements IValueBearing { 106 | identifiers: string[]; 107 | 108 | constructor() { 109 | super(); 110 | this.identifiers = ['assets']; 111 | } 112 | 113 | async extract(chain: IChain, account: string): Promise { 114 | const { api, name: chainName } = chain; 115 | const assets: Asset[] = []; 116 | const allAssetIds = (await api.query.assets.asset.entries()).map((a) => a[0].args[0]); 117 | const fetchAssetsPromise = allAssetIds.map(async (assetId) => { 118 | const assetAccount = await api.query.assets.account(assetId, account); 119 | if (assetAccount.isSome && !assetAccount.unwrap().balance.isZero()) { 120 | const meta = await api.query.assets.metadata(assetId); 121 | const decimals = new BN(meta.decimals); 122 | const ticker = (meta.symbol.toHuman() || '').toString(); 123 | const price = await tickerPrice(ticker); 124 | assets.push( 125 | new Asset({ 126 | ticker, 127 | name: ticker, 128 | price, 129 | transferrable: Boolean(assetAccount.unwrap().isFrozen), 130 | amount: assetAccount.unwrap().balance, 131 | decimals, 132 | origin: { account, chain: chainName, source: 'assets pallet' } 133 | }) 134 | ); 135 | } 136 | }); 137 | await Promise.all(fetchAssetsPromise); 138 | 139 | return assets; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/src/index.ts: -------------------------------------------------------------------------------- 1 | import '@polkadot/api-augment'; 2 | export * from './types'; 3 | export * from './utils'; 4 | export * from './fetch'; 5 | export * from './endpoints'; 6 | export { ApiPromise, WsProvider } from '@polkadot/api'; 7 | export { isAddress } from '@polkadot/util-crypto'; 8 | -------------------------------------------------------------------------------- /core/src/types.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { tickerPrice } from './utils'; 3 | 4 | /// Origin of an asset. 5 | export interface AssetOrigin { 6 | /// The owner. 7 | account: string; 8 | /// The chain to which it belongs. 9 | chain: string; 10 | /// The source pallet from which this asset has been extracted. 11 | source: string; 12 | } 13 | 14 | interface IAsset { 15 | name: string; 16 | ticker: string; 17 | amount: BN; 18 | transferrable: boolean; 19 | price: number; 20 | decimals: BN; 21 | origin: AssetOrigin; 22 | } 23 | 24 | /// A value bearing asset, existing in some blockchain system. 25 | export class Asset { 26 | /// The ticker of this asset. Usually a 3-4 character ticker, e.g. BTC. 27 | ticker: string; 28 | /// Longer version of the name of this asset, e.g. Bitcoin. 29 | name: string; 30 | /// The amount of asset. 31 | amount: BN; 32 | /// The decimal points of this asset. 33 | /// 34 | /// For example, DOT has 10 decimal points, so when amount = 10^12, it is equal to 100 DOTs. 35 | /// 36 | /// See `decimal()` and `fraction()` methods. 37 | decimals: BN; 38 | /// Indicates if this asset is transferrable or not. 39 | transferrable: boolean; 40 | /// Last known price of this asset. 41 | price: number; 42 | /// Origin details of the asset. 43 | origin: AssetOrigin; 44 | 45 | constructor(asset: IAsset) { 46 | this.name = asset.name; 47 | this.ticker = asset.ticker; 48 | this.amount = asset.amount; 49 | this.transferrable = asset.transferrable; 50 | this.amount = asset.amount; 51 | this.price = asset.price; 52 | this.decimals = asset.decimals; 53 | this.origin = asset.origin; 54 | } 55 | 56 | /// Refresh the price of this asset. Updated `this.price`. 57 | async refreshPrice() { 58 | this.price = await tickerPrice(this.ticker); 59 | } 60 | 61 | // the decimal part of the amount, e.g. `123` in `123.xxx` 62 | decimal(): BN { 63 | return this.amount.div(new BN(10).pow(this.decimals)); 64 | } 65 | 66 | // the (per-thousand) fractional part of the amount, e.g. `123` in `x.123` 67 | fraction(): BN { 68 | return this.amount.div(new BN(10).pow(this.decimals.sub(new BN(3)))).mod(new BN(1000)); 69 | } 70 | 71 | // combination of `decimal` and `fraction`, returned as a float number. 72 | floatAmount(): number { 73 | return parseFloat(`${this.decimal()}.${this.fraction().toString()}`); 74 | } 75 | 76 | euroValue(): number { 77 | if (this.price === 0) { 78 | return 0; 79 | } 80 | const d = new BN(1000); 81 | const scaledValue = this.amount.mul(d).div(new BN(10).pow(this.decimals)).toNumber(); 82 | return (scaledValue * this.price) / 1000; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise } from '@polkadot/api'; 2 | import axios from 'axios'; 3 | import BN from 'bn.js'; 4 | 5 | const PRICE_CACHE: Map = new Map(); 6 | 7 | // TODO: get rid of this. This is only here to fulfill coingecko's shitty API. 8 | const COINGECKO_MAP: Map = new Map([ 9 | ['ksm', 'kusama'], 10 | ['kar', 'karura'], 11 | ['astr', 'astar'], 12 | ['dot', 'polkadot'], 13 | ['movr', 'moonriver'], 14 | ['glmr', 'moonbeam'], 15 | ['aca', 'acala'], 16 | ['para', 'parallel'], 17 | ['azero', 'aleph-zero'], 18 | ['sub', 'subsocial'], 19 | ['efi', 'efinity'], 20 | ['cfg', 'centrifuge'] 21 | ]); 22 | 23 | const doGetPrice = async (query: string): Promise => { 24 | try { 25 | const data = await axios.get(`https://api.coingecko.com/api/v3/coins/${query}`); 26 | const price = data.data['market_data']['current_price']['eur']; 27 | PRICE_CACHE.set(query, price); 28 | console.log(`💳 got the price of ${query} as ${price}`); 29 | return price; 30 | } catch (e) { 31 | PRICE_CACHE.set(query, 0); 32 | console.log(`⛔️ failed to get the price of ${query}: ${e}`); 33 | return 0; 34 | } 35 | }; 36 | 37 | export async function queryPrice(whatever: string): Promise { 38 | const tickerTry = await tickerPrice(whatever); 39 | if (tickerTry === 0) { 40 | return await chainPrice(whatever); 41 | } else { 42 | return tickerTry; 43 | } 44 | } 45 | 46 | export async function tickerPrice(ticker: string): Promise { 47 | // cater for xc-tokens in moonbeam. 48 | if (ticker.startsWith('xc')) { 49 | ticker = ticker.slice(2); 50 | } 51 | 52 | const normalized = COINGECKO_MAP.has(ticker.toLowerCase()) 53 | ? COINGECKO_MAP.get(ticker.toLowerCase())! 54 | : ticker.toLowerCase(); 55 | 56 | if (PRICE_CACHE.has(normalized)) { 57 | return PRICE_CACHE.get(normalized)!; 58 | } 59 | return await doGetPrice(normalized); 60 | } 61 | 62 | export async function chainPrice(chain: string): Promise { 63 | const normalized = chain.toLowerCase(); 64 | 65 | if (PRICE_CACHE.has(normalized)) { 66 | return PRICE_CACHE.get(normalized)!; 67 | } 68 | return await doGetPrice(normalized); 69 | } 70 | 71 | export function findDecimals(api: ApiPromise, token: string): BN { 72 | const index = api.registry.chainTokens.findIndex((t) => t.toLowerCase() == token.toLowerCase()); 73 | return index > -1 ? new BN(api.registry.chainDecimals[index]) : new BN(1); 74 | } 75 | 76 | export function currencyFormat(num: number, currency = 'EUR', locale = 'en-US'): string { 77 | const formatter = new Intl.NumberFormat(locale, { 78 | style: 'currency', 79 | currency 80 | }); 81 | return formatter.format(num); 82 | } 83 | -------------------------------------------------------------------------------- /core/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { allChains, scrape, makeApi } from '../src/index'; 2 | 3 | describe('allChain', () => { 4 | test('should not be empty', () => { 5 | expect(allChains.length).toBeGreaterThan(0); 6 | }); 7 | }); 8 | 9 | describe('scrape', async () => { 10 | test('should fetch treasury account', async () => { 11 | const { api } = await makeApi('wss://rpc.polkadot.io'); 12 | const assets = await scrape('13UVJyLnbVp9RBZYFwFGyDvVd1y27Tt8tkntv6Q7JVPhFsTB', api); 13 | expect(assets.length).toBeGreaterThan(0); 14 | }); 15 | 16 | test('should fetch acala tokens', async () => { 17 | const { api } = await makeApi('wss://karura.api.onfinality.io/public-ws'); 18 | const assets = await scrape('HL8bEp8YicBdrUmJocCAWVLKUaR2dd1y6jnD934pbre3un1', api); 19 | expect(assets.length).toBeGreaterThan(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "lib": ["es6", "dom", "es2016", "es2017", "es2018", "es2019", "es2020"], 6 | "moduleResolution": "node", 7 | "allowJs": true, 8 | "rootDir": "./src", 9 | 10 | // Some compiling checks. 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | 19 | // dep resolution 20 | "allowSyntheticDefaultImports": true, 21 | "esModuleInterop": true, 22 | "experimentalDecorators": true, 23 | "resolveJsonModule": false, 24 | 25 | "declaration": false, 26 | "emitDeclarationOnly": false, 27 | "outDir": "build", 28 | "declarationMap": false, 29 | "skipLibCheck": true 30 | }, 31 | "exclude": ["**/*.test.ts", "*.js", "build", "jest.config.js"], 32 | } 33 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polkadot-portfolio", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /ui/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:react/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "env": { 15 | "browser": true, 16 | "es2021": true, 17 | "jest": true 18 | }, 19 | "rules": { 20 | "react/react-in-jsx-scope": "off", 21 | "@typescript-eslint/ban-ts-comment": "warn", 22 | "indent": "off", 23 | "react/prop-types": 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | ./node_modules 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "printWidth": 100, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true 9 | } -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 7 | "@fortawesome/free-solid-svg-icons": "^6.1.1", 8 | "@fortawesome/react-fontawesome": "^0.1.18", 9 | "@polkadot/react-identicon": "^2.9.1", 10 | "@testing-library/jest-dom": "^5.16.4", 11 | "@testing-library/react": "^13.1.1", 12 | "@testing-library/user-event": "^13.5.0", 13 | "@types/jest": "^27.4.1", 14 | "@types/node": "^16.11.27", 15 | "@types/react": "^18.0.5", 16 | "@types/react-dom": "^18.0.1", 17 | "bn.js": "^5.2.0", 18 | "classnames": "^2.3.1", 19 | "currency-formatter": "^1.5.9", 20 | "polkadot-portfolio-core": "../core", 21 | "react": "^18.0.0", 22 | "react-dom": "^18.0.0", 23 | "react-scripts": "5.0.1", 24 | "typescript": "^4.6.3", 25 | "web-vitals": "^2.1.4" 26 | }, 27 | "devDependencies": { 28 | "@types/currency-formatter": "^1.5.1", 29 | "@types/lodash": "^4.14.182", 30 | "autoprefixer": "^10.4.7", 31 | "eslint-config-prettier": "^8.5.0", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "postcss": "^8.4.13", 34 | "prettier": "^2.7.1", 35 | "tailwindcss": "^3.0.24" 36 | }, 37 | "scripts": { 38 | "start": "react-scripts start", 39 | "build": "react-scripts build", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject", 42 | "lint": "eslint . --ext .ts --fix", 43 | "lint:fix": "eslint --fix", 44 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", 45 | "link": "link ../core" 46 | }, 47 | "eslintConfig": { 48 | "extends": [ 49 | "react-app", 50 | "react-app/jest" 51 | ] 52 | }, 53 | "homepage": "https://substrate-portfolio.github.io/polkadot-portfolio", 54 | "browserslist": { 55 | "production": [ 56 | "chrome >= 67", 57 | "edge >= 79", 58 | "firefox >= 68", 59 | "opera >= 54", 60 | "safari >= 14" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /ui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/favicon-16x16.png -------------------------------------------------------------------------------- /ui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/favicon-32x32.png -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Polkadot Asset Portfolio 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/substrate-portfolio/polkadot-portfolio/d40a58995c19c9afc780ba3a66af43a83194bb9b/ui/public/logo512.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/src/components/About/index.tsx: -------------------------------------------------------------------------------- 1 | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { useCallback, useState } from 'react'; 4 | import Modal from '../Modal'; 5 | import ModalBox from '../ModalBox'; 6 | 7 | const Styles = { 8 | wrapper: 'py-2 px-2 border-t-2 border-gray-100 mt-auto', 9 | button: 'text-gray-500 hover:text-gray-800 inline-block py-2 px-2 cursor-pointer', 10 | icon: 'mx-2', 11 | link: 'text-gray-500 hover:text-gray-800', 12 | paragraph: 'mb-2' 13 | }; 14 | 15 | const About = () => { 16 | const [modalState, setModalState] = useState(false); 17 | const handleModalState = useCallback( 18 | (state: boolean) => () => { 19 | setModalState(state); 20 | }, 21 | [] 22 | ); 23 | return ( 24 |
25 | 26 | 27 | About Project 28 | 29 | 30 | 31 |
32 |

33 | A tool to find all your bags of tokens im the highly complicated world of Polkadot 34 | Ecosystem. 35 |

36 |

37 | This tool is completely open source. Feel free to contribute to the project in our{' '} 38 | 43 | Github 44 | {' '} 45 | page. 46 |

47 |

48 | Created by{' '} 49 | 54 | Kian Paimani 55 | {' '} 56 | &{' '} 57 | 62 | Hosein Emrani 63 | {' '} 64 | . 65 |

66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default About; 74 | -------------------------------------------------------------------------------- /ui/src/components/AccountIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import Identicon from '@polkadot/react-identicon'; 2 | 3 | interface AccountIconProps { 4 | address: string; 5 | size?: number; 6 | theme?: NetworkThemes; 7 | } 8 | 9 | export enum NetworkThemes { 10 | PolkaDot = 'polkadot', 11 | Substrate = 'substrate', 12 | BeachBall = 'beachball', 13 | JDentIcon = 'jdenticon' 14 | } 15 | 16 | const AccountIcon: React.FC = (props: AccountIconProps) => { 17 | const { address, size, theme } = props; 18 | const DefaultSize = 24; 19 | const DefaultTheme = NetworkThemes.PolkaDot; 20 | return ( 21 | 27 | ); 28 | }; 29 | 30 | export default AccountIcon; 31 | -------------------------------------------------------------------------------- /ui/src/components/Accounts/AccountListSettings.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useCallback, useContext, useMemo, useState } from 'react'; 3 | import { AppContext } from '../../store'; 4 | import { SharedStyles } from '../../utils/styles'; 5 | import ModalBox from '../ModalBox'; 6 | import { isAddress } from 'polkadot-portfolio-core'; 7 | 8 | const AccountListSettings = () => { 9 | const { actions } = useContext(AppContext); 10 | const { addAccount } = actions; 11 | const [name, setNameInput] = useState(''); 12 | const [stash, setIdInput] = useState(''); 13 | 14 | const disabled = useMemo(() => { 15 | const lengthCondition = name.length > 1 && stash.length > 1; 16 | const addressCondition = stash.length > 0 ? isAddress(stash) : false; 17 | 18 | return !(lengthCondition && addressCondition); 19 | }, [name, stash]); 20 | 21 | const addAccountToList = useCallback(async () => { 22 | if (disabled) return; 23 | addAccount({ 24 | name, 25 | id: stash 26 | }); 27 | setNameInput(''); 28 | 29 | setIdInput(''); 30 | }, [addAccount, name, stash, disabled]); 31 | 32 | const handleStash = useCallback((event: React.ChangeEvent) => { 33 | setIdInput(event.target.value); 34 | }, []); 35 | 36 | const handleName = useCallback((event: React.ChangeEvent) => { 37 | setNameInput(event.target.value); 38 | }, []); 39 | 40 | return ( 41 | 42 |
43 | 49 | 55 |
56 | 64 |
65 | ); 66 | }; 67 | 68 | export default AccountListSettings; 69 | -------------------------------------------------------------------------------- /ui/src/components/Accounts/AccountsList.tsx: -------------------------------------------------------------------------------- 1 | import { faClose, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import classNames from 'classnames'; 4 | import { useCallback, useContext } from 'react'; 5 | import { AppContext } from '../../store'; 6 | import AccountIcon from '../AccountIcon'; 7 | 8 | const AccountsList = () => { 9 | const { state, actions } = useContext(AppContext); 10 | const { accounts, visibility } = state; 11 | const { accounts: hiddenAccounts } = visibility; 12 | const { removeAccount, changeVisibility } = actions; 13 | 14 | const handleRemove = useCallback( 15 | (stash: string) => () => { 16 | removeAccount(stash); 17 | }, 18 | [removeAccount] 19 | ); 20 | 21 | const handleVisibility = useCallback( 22 | (account: string) => () => { 23 | changeVisibility(account); 24 | }, 25 | [changeVisibility] 26 | ); 27 | 28 | if (accounts.length) 29 | return ( 30 |
31 | {accounts.map((account, index) => ( 32 |
35 |
36 | 37 | {account.name} 38 |
39 | 40 |
41 |
44 | 51 |
52 |
55 | 56 |
57 |
58 |
59 | ))} 60 |
61 | ); 62 | 63 | return

Please add some accounts

; 64 | }; 65 | 66 | export default AccountsList; 67 | -------------------------------------------------------------------------------- /ui/src/components/Accounts/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { faPlus } from '@fortawesome/free-solid-svg-icons'; 3 | import React, { useState } from 'react'; 4 | import Modal from '../Modal'; 5 | import AccountsList from './AccountsList'; 6 | import AccountListSettings from './AccountListSettings'; 7 | 8 | export const Accounts = () => { 9 | const [modalOpen, setModalState] = useState(false); 10 | 11 | const handleModalState = React.useCallback( 12 | (state: boolean) => () => { 13 | setModalState(state); 14 | }, 15 | [] 16 | ); 17 | 18 | return ( 19 |
20 |
21 | Accounts 22 | 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /ui/src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useBackgroundFetch from '../../hooks/useBackgroundFetch'; 3 | import Dashboard from '../../views/Dashboard'; 4 | function App() { 5 | useBackgroundFetch(); 6 | 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /ui/src/components/Assets/AssetList.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { IAccount } from '../../store/store'; 3 | import { Asset, currencyFormat, ApiPromise } from 'polkadot-portfolio-core'; 4 | import { AssetGroups, tableHeads } from '../../utils/constants'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; 7 | import classNames from 'classnames'; 8 | 9 | interface AssetListProps { 10 | assets: Asset[]; 11 | accounts: IAccount[]; 12 | apiRegistry: Map; 13 | groupBy: AssetGroups | null; 14 | } 15 | 16 | interface AssetItemProps { 17 | asset: Asset; 18 | accounts: IAccount[]; 19 | apiRegistry: Map; 20 | } 21 | 22 | const styles = { 23 | tableHead: 24 | 'flex-1 w-full p-2 font-bold cursor-pointer hover:bg-gray-50 inline-flex items-center justify-between', 25 | emptyBox: 'px-4 py-4 text-lg text-center', 26 | row: 'flex justify-between w-full hover:bg-gray-50 border-b', 27 | col: 'flex-1 w-full p-2 text-ellipsis overflow-hidden', 28 | tableBody: 'flex justify-between w-full border-b-2' 29 | }; 30 | 31 | export const AssetItem = ({ asset, accounts, apiRegistry }: AssetItemProps) => { 32 | const accountName = useMemo(() => { 33 | const account = accounts.find((item) => item.id == asset.origin.account); 34 | return account?.name ?? ''; 35 | }, [accounts]); 36 | 37 | return ( 38 |
39 | 40 | {asset.name} 41 | 42 | {asset.ticker} 43 | {accountName} 44 | {asset.origin.chain} 45 | {asset.origin.source} 46 | {asset.floatAmount()} 47 | {currencyFormat(asset.euroValue())} 48 |
49 | ); 50 | }; 51 | 52 | const filterZeroAmount = (item: Asset) => item.floatAmount() > 0; 53 | 54 | const sortTable = 55 | (sortOrder: AssetGroups, asc: boolean) => 56 | (a: Asset, b: Asset): number => { 57 | let orderNumber: number; 58 | switch (sortOrder) { 59 | case AssetGroups.Name: 60 | orderNumber = b.name.localeCompare(a.name); 61 | break; 62 | case AssetGroups.Token: 63 | orderNumber = b.ticker.localeCompare(a.ticker); 64 | break; 65 | case AssetGroups.Account: 66 | orderNumber = b.origin.account.localeCompare(a.origin.account); 67 | break; 68 | case AssetGroups.Chain: 69 | orderNumber = b.origin.chain.localeCompare(a.origin.chain); 70 | break; 71 | case AssetGroups.Source: 72 | orderNumber = b.origin.source.localeCompare(a.origin.source); 73 | break; 74 | case AssetGroups.Amount: 75 | orderNumber = b.floatAmount() - a.floatAmount(); 76 | break; 77 | case AssetGroups.Value: 78 | orderNumber = b.euroValue() - a.euroValue(); 79 | break; 80 | } 81 | 82 | if (asc) return orderNumber * -1; 83 | return orderNumber; 84 | }; 85 | 86 | // TODO: IT WAS BIGGER THAN I INITIALLY THOUGHT, GOING TO IMPLEMENT IT LATER. 87 | // const groupAssetsBy = (assetGroup: AssetGroups) => (sum: Asset[], asset: Asset, index: number): Asset[] => { 88 | // switch (assetGroup) { 89 | // case AssetGroups.Token: 90 | // break; 91 | // case AssetGroups.Account: 92 | // break; 93 | // case AssetGroups.Chain: 94 | // break; 95 | // case AssetGroups.Source: 96 | // break; 97 | // case AssetGroups.Amount: 98 | // break; 99 | // case AssetGroups.Value: 100 | // break; 101 | // } 102 | // } 103 | 104 | export const AssetList = ({ assets, accounts, apiRegistry }: AssetListProps) => { 105 | const [sortOrder, setSortOrder] = useState(AssetGroups.Value); 106 | const [asc, setAsc] = useState(false); 107 | const filteredAssets = useMemo(() => { 108 | return assets.filter(filterZeroAmount).sort(sortTable(sortOrder, asc)); 109 | // if(!groupBy) return sortedAndFiltered; 110 | // return sortedAndFiltered.reduce(groupAssetsBy(groupBy), []) 111 | }, [assets, sortOrder, asc]); 112 | 113 | const updateSortOrder = useCallback( 114 | (order: AssetGroups) => () => { 115 | if (sortOrder === order) setAsc((prev) => !prev); 116 | else { 117 | setSortOrder(order); 118 | setAsc(true); 119 | } 120 | }, 121 | [sortOrder] 122 | ); 123 | 124 | if (filteredAssets.length <= 0) 125 | return
No Valued Asset found
; 126 | 127 | return ( 128 |
129 |
130 | {tableHeads.map((th, index) => ( 131 | 135 | {th.title} 136 | {sortOrder === th.assetGroup ? ( 137 | 142 | ) : null} 143 | 144 | ))} 145 |
146 | {filteredAssets.map((asset, index) => ( 147 | 153 | ))} 154 |
155 | ); 156 | }; 157 | -------------------------------------------------------------------------------- /ui/src/components/Assets/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useCallback, useContext, useMemo, useState } from 'react'; 3 | import { AppContext } from '../../store'; 4 | import { IVisibility } from '../../store/store'; 5 | import { Asset, currencyFormat } from 'polkadot-portfolio-core'; 6 | import { AssetGroups, tableHeads } from '../../utils/constants'; 7 | import { AssetList } from './AssetList'; 8 | 9 | const filterVisibility = 10 | (visibility: IVisibility) => 11 | (item: Asset, index: number, array: Asset[]): boolean => 12 | !( 13 | visibility.accounts.includes(item.origin.account) || 14 | visibility.networks.includes(item.origin.chain) 15 | ); 16 | 17 | const Assets = () => { 18 | const { 19 | state: { accounts, apiRegistry, assets, visibility } 20 | } = useContext(AppContext); 21 | const filteredAssets = useMemo( 22 | () => assets.filter(filterVisibility(visibility)), 23 | [assets, visibility] 24 | ); 25 | const totalAssetValuesInAllChains = useMemo(() => { 26 | const sum = filteredAssets.reduce((sum, asset) => sum + asset.euroValue(), 0); 27 | return currencyFormat(sum); 28 | }, [filteredAssets]); 29 | 30 | const [groupBy, setGroupBy] = useState(null); 31 | // const groupAssetsBy = useCallback((gb: AssetGroups | null) => () => { 32 | // if(groupBy === gb) setGroupBy(null) 33 | // else setGroupBy(gb) 34 | // }, [groupBy]) 35 | 36 | return ( 37 |
38 |
39 |
40 |
41 |
Total Asset Value in All Chains:
42 |
43 | {totalAssetValuesInAllChains} 44 |
45 |
46 | {/*
47 |
48 |
Group Assets:
49 |
50 | {tableHeads.map((item, index) => ( 51 | 56 | {item.title} 57 | 58 | ))} 59 |
60 |
61 |
*/} 62 |
63 |
64 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Assets; 77 | -------------------------------------------------------------------------------- /ui/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'; 3 | import { useContext, useMemo } from 'react'; 4 | import { AppContext } from '../../store'; 5 | 6 | const Header = () => { 7 | const { 8 | state: { loading } 9 | } = useContext(AppContext); 10 | 11 | const anyLoading = useMemo( 12 | () => Object.values(loading).reduce((sum, state) => state || sum, false), 13 | [loading] 14 | ); 15 | 16 | return ( 17 |
18 |
19 |
20 |
21 | 22 | Asset Portfolio 23 | 24 | {anyLoading ? ( 25 | 26 | 27 | 28 | ) : null} 29 |
30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Header; 37 | -------------------------------------------------------------------------------- /ui/src/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useClickOutside from '../../hooks/clickOutside'; 3 | interface ModalProps { 4 | state: boolean; 5 | children: any; 6 | closeFn: () => void; 7 | onClose?: (modalState: boolean) => void; 8 | } 9 | 10 | const Modal: React.FC = (props) => { 11 | const { state, onClose, closeFn, children } = props; 12 | 13 | const wrapperRef = React.useRef(null); 14 | 15 | const closeModal: (_: Event) => void = React.useCallback(() => { 16 | console.log('CLOSE MODAL CALLED'); 17 | closeFn(); 18 | onClose && onClose(state); 19 | }, []); 20 | 21 | useClickOutside(wrapperRef, closeModal); 22 | 23 | if (!state) return null; 24 | 25 | return ( 26 |
27 |
28 | {children} 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Modal; 35 | -------------------------------------------------------------------------------- /ui/src/components/ModalBox/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ModalBoxProps { 4 | title: string; 5 | children: any[] | any; 6 | } 7 | 8 | const ModalBox: React.FC = ({ title, children }) => { 9 | return ( 10 |
11 |
12 |

{title}

13 |
14 | <> 15 | {Array.isArray(children) 16 | ? children.map((item, index) => ( 17 | {item} 18 | )) 19 | : children} 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default ModalBox; 26 | -------------------------------------------------------------------------------- /ui/src/components/Networks/ManualNetworkInput.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useCallback } from 'react'; 3 | import { SharedStyles as styles } from '../../utils/styles'; 4 | 5 | interface ManualNetworkProps { 6 | networkInput: string; 7 | setNetworkInput: (wss: string) => void; 8 | validNetwork: boolean; 9 | setValidNetworkState: (state: boolean) => void; 10 | } 11 | 12 | const ManualNetwork: React.FC = ({ 13 | networkInput, 14 | setNetworkInput, 15 | validNetwork 16 | }) => { 17 | const handleInput = useCallback(({ target: { value } }: React.ChangeEvent) => { 18 | setNetworkInput(value); 19 | }, []); 20 | return ( 21 |
22 |

Add your network WSS address.

23 | 32 | {!validNetwork && networkInput.length ? ( 33 |

Please enter a valid web socket address.

34 | ) : null} 35 |
36 | ); 37 | }; 38 | 39 | export default ManualNetwork; 40 | -------------------------------------------------------------------------------- /ui/src/components/Networks/NetworkLookup.tsx: -------------------------------------------------------------------------------- 1 | import { allChains, Ecosystem, IChainEndpoint } from 'polkadot-portfolio-core'; 2 | import { useCallback, useEffect, useMemo, useState } from 'react'; 3 | import { SharedStyles as styles } from '../../utils/styles'; 4 | 5 | interface NetworkLookupProps { 6 | setNetworkInput: (input: string) => void; 7 | } 8 | 9 | interface NormalizedNetworkEndpoint { 10 | name: string; 11 | ecosystem: Ecosystem; 12 | endpointName: string; 13 | endpointAddress: string; 14 | } 15 | 16 | const normalizeString = (input: string): string => input.trim().toLowerCase(); 17 | 18 | const mapToEndpoints = 19 | (name: string, ecosystem: Ecosystem) => 20 | ([endpointName, endpointAddress]: [string, string]): NormalizedNetworkEndpoint => 21 | ({ name, ecosystem, endpointName, endpointAddress } as NormalizedNetworkEndpoint); 22 | 23 | const normalizeChains = (chains: IChainEndpoint[]): NormalizedNetworkEndpoint[] => { 24 | return chains.reduce((prev, current) => { 25 | const endpoints = Object.entries(current.endpoints).map( 26 | mapToEndpoints(current.name, current.ecosystem) 27 | ); 28 | return [...prev, ...endpoints]; 29 | }, [] as NormalizedNetworkEndpoint[]); 30 | }; 31 | 32 | const filterNetworks = 33 | (lookup: string) => 34 | (item: NormalizedNetworkEndpoint): boolean => { 35 | const normalName = normalizeString(item.name); 36 | const normalEcoSystem = normalizeString(item.ecosystem); 37 | const normalEndpointName = normalizeString(item.endpointName); 38 | const normalAddress = normalizeString(item.endpointAddress); 39 | const lookups = lookup 40 | .split('/') 41 | .map(normalizeString) 42 | .filter((item) => item !== ''); 43 | const isInName = lookups.some((normalLookup) => normalName.includes(normalLookup)); 44 | const isInEcosystem = lookups.some((normalLookup) => normalEcoSystem.includes(normalLookup)); 45 | const isInEndpointName = lookups.some((normalLookup) => 46 | normalEndpointName.includes(normalLookup) 47 | ); 48 | const isInEndpointAddress = lookups.some((normalLookup) => 49 | normalAddress.includes(normalLookup) 50 | ); 51 | 52 | return isInName || isInEcosystem || isInEndpointName || isInEndpointAddress; 53 | }; 54 | 55 | const NetworkLookup: React.FC = ({ setNetworkInput }) => { 56 | const [lookupInput, setLookupInput] = useState(''); 57 | const [networks, setNetworks] = useState([]); 58 | 59 | const [dropdownVisible, setDropdownVisibility] = useState(false); 60 | 61 | const handleLookupInput = useCallback((event: React.ChangeEvent) => { 62 | const { value: lookup } = event.target; 63 | setDropdownVisibility(true); 64 | setLookupInput(lookup); 65 | }, []); 66 | 67 | const handleSelect = useCallback( 68 | (network: NormalizedNetworkEndpoint) => () => { 69 | const networkInput = `${network.ecosystem} / ${network.name} / ${network.endpointName}`; 70 | setLookupInput(networkInput); 71 | setNetworkInput(network.endpointAddress); 72 | setDropdownVisibility(false); 73 | }, 74 | [] 75 | ); 76 | 77 | const normalizedChains = useMemo(() => normalizeChains(allChains), []); 78 | 79 | useEffect(() => { 80 | const filteredNetworks = normalizedChains.filter(filterNetworks(lookupInput)); 81 | setNetworks(filteredNetworks); 82 | }, [lookupInput]); 83 | 84 | return ( 85 |
86 | 93 | {networks.length > 0 && dropdownVisible ? ( 94 |
95 |
96 | {networks.map((item, index) => ( 97 |
101 | {item.ecosystem}/ 102 | {item.name}/ 103 | {item.endpointName} 104 |
105 | ))} 106 |
107 |
108 | ) : null} 109 |
110 | ); 111 | }; 112 | 113 | export default NetworkLookup; 114 | -------------------------------------------------------------------------------- /ui/src/components/Networks/NetworkSettings.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useCallback, useEffect, useMemo, useState } from 'react'; 3 | import { FAddNetwork } from '../../store/store'; 4 | import { SharedStyles as styles } from '../../utils/styles'; 5 | import { validateWebsocketUrls } from '../../utils/validators'; 6 | import ModalBox from '../ModalBox'; 7 | import ManualNetwork from './ManualNetworkInput'; 8 | import NetworkLookup from './NetworkLookup'; 9 | 10 | export interface AddNetworkProps { 11 | addNetwork: FAddNetwork; 12 | } 13 | 14 | const NetworksSetting = ({ addNetwork }: AddNetworkProps) => { 15 | const [validNetwork, setValidNetworkState] = useState(false); 16 | const [networkInput, setNetworkInput] = useState(''); 17 | const [manualMode, setManualMode] = useState(false); 18 | 19 | const addNetworkToList = useCallback(async () => { 20 | if (!validNetwork) return; 21 | addNetwork(networkInput); 22 | setNetworkInput(''); 23 | }, [addNetwork, networkInput, validNetwork]); 24 | 25 | const toggleManualNetwork = useCallback(() => { 26 | setManualMode((prev) => !prev); 27 | }, []); 28 | 29 | useEffect(() => { 30 | const isValid = validateWebsocketUrls(networkInput); 31 | setValidNetworkState(isValid); 32 | }, [networkInput]); 33 | 34 | const toggleButtonText = useMemo( 35 | () => (!manualMode ? 'Enter URL Manually' : 'Search Networks'), 36 | [manualMode] 37 | ); 38 | 39 | return ( 40 | 41 | {manualMode ? ( 42 | 48 | ) : ( 49 | 50 | )} 51 |
52 |
55 | {toggleButtonText} 56 |
57 | 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default NetworksSetting; 71 | -------------------------------------------------------------------------------- /ui/src/components/Networks/NetworksList.tsx: -------------------------------------------------------------------------------- 1 | import { faClose, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { ApiPromise } from 'polkadot-portfolio-core'; 4 | import classNames from 'classnames'; 5 | import { useCallback, useMemo } from 'react'; 6 | import { 7 | FChangeVisibility, 8 | FRemoveApiRegistry, 9 | FRemoveNetwork, 10 | IVisibility 11 | } from '../../store/store'; 12 | import AccountIcon from '../AccountIcon'; 13 | 14 | interface NetworkItemProps { 15 | key: string; 16 | network: string; 17 | registry: ApiPromise | undefined; 18 | visibility: IVisibility; 19 | disconnect: (network: string) => () => void; 20 | handleVisibility: (network: string) => () => void; 21 | } 22 | 23 | const NetworkItem = ({ 24 | key, 25 | network, 26 | visibility, 27 | registry, 28 | disconnect, 29 | handleVisibility 30 | }: NetworkItemProps) => { 31 | const networkName = useMemo(() => registry?.runtimeChain ?? network, [network, registry]); 32 | const { networks: hiddenNetworks } = visibility; 33 | const isConnected = useMemo(() => registry?.isConnected, [registry]); 34 | return ( 35 |
36 |
37 | 41 | 42 | {networkName.length > 20 ? `${networkName.slice(0, 20)}...` : networkName} 43 | 44 |
45 |
46 | 53 |
54 |
57 | 58 |
59 |
60 |
61 |
62 | ); 63 | }; 64 | 65 | export interface NetworksListProps { 66 | networks: string[]; 67 | registry: Map; 68 | visibility: IVisibility; 69 | removeNetwork: FRemoveNetwork; 70 | removeRegistry: FRemoveApiRegistry; 71 | changeVisibility: FChangeVisibility; 72 | } 73 | 74 | const NetworksList = (props: NetworksListProps) => { 75 | const { networks, registry, visibility, removeNetwork, removeRegistry, changeVisibility } = props; 76 | 77 | const handleDisconnect = useCallback( 78 | (network: string) => () => { 79 | removeNetwork(network); 80 | removeRegistry(network); 81 | }, 82 | [removeNetwork] 83 | ); 84 | 85 | const handleVisibility = useCallback( 86 | (network: string) => () => { 87 | changeVisibility(null, network); 88 | }, 89 | [changeVisibility] 90 | ); 91 | 92 | if (networks.length) 93 | return ( 94 |
95 | {networks.map((network, index) => ( 96 | 104 | ))} 105 |
106 | ); 107 | 108 | return

Please add some networks

; 109 | }; 110 | 111 | export default NetworksList; 112 | -------------------------------------------------------------------------------- /ui/src/components/Networks/index.tsx: -------------------------------------------------------------------------------- 1 | import { faPlus } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import React, { useContext, useState } from 'react'; 4 | import { AppContext } from '../../store'; 5 | import Modal from '../Modal'; 6 | import NetworksSetting from './NetworkSettings'; 7 | import NetworksList from './NetworksList'; 8 | 9 | const Networks = () => { 10 | const { state, actions } = useContext(AppContext); 11 | const { networks, apiRegistry, visibility } = state; 12 | const { removeNetwork, addNetwork, changeVisibility, removeApiRegistry } = actions; 13 | const [modalOpen, setModalState] = useState(false); 14 | 15 | const handleModalState = React.useCallback( 16 | (state: boolean) => () => { 17 | setModalState(state); 18 | }, 19 | [] 20 | ); 21 | 22 | return ( 23 |
24 |
25 | Chains 26 | 32 |
33 |
34 | 42 |
43 | 44 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default Networks; 51 | -------------------------------------------------------------------------------- /ui/src/hooks/clickOutside.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | 3 | const useClickOutside = ( 4 | ref: React.MutableRefObject, 5 | clickOutsideCallback: (event: Event) => void 6 | ) => { 7 | const handleClickOutside = useCallback( 8 | (event: MouseEvent) => { 9 | if (ref.current && !ref.current.contains(event.target)) { 10 | clickOutsideCallback(event); 11 | } 12 | }, 13 | [clickOutsideCallback] 14 | ); 15 | 16 | useEffect(() => { 17 | // Bind the event listener 18 | document.addEventListener('mousedown', handleClickOutside); 19 | return () => { 20 | // Unbind the event listener on clean up 21 | document.removeEventListener('mousedown', handleClickOutside); 22 | }; 23 | }, [ref]); 24 | }; 25 | 26 | export default useClickOutside; 27 | -------------------------------------------------------------------------------- /ui/src/hooks/useBackgroundFetch.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useState } from 'react'; 2 | import { AppContext } from '../store'; 3 | import { IAccount, LoadingScope } from '../store/store.d'; 4 | import { Asset, scrape, makeApi, ApiPromise } from 'polkadot-portfolio-core'; 5 | import { usePrevious } from './usePrevious'; 6 | 7 | const useSyncNetworks = (useTrigger: TriggerRefresh) => { 8 | const [_, setRefresh] = useTrigger; 9 | const { state, actions } = useContext(AppContext); 10 | const { networks, apiRegistry } = state; 11 | const { addApiRegistry, setLoading } = actions; 12 | 13 | const connect = async (networkUri: string): Promise => { 14 | const { api } = await makeApi(networkUri); 15 | return api; 16 | }; 17 | 18 | const setupNetwork = useCallback(async (network: string) => { 19 | setLoading(true, LoadingScope.networks); 20 | const api = await connect(network); 21 | addApiRegistry(network, api); 22 | setRefresh((prevState) => prevState + 1); 23 | setLoading(false, LoadingScope.networks); 24 | }, []); 25 | 26 | useEffect(() => { 27 | for (const network of networks) { 28 | if (!apiRegistry.has(network)) setupNetwork(network); 29 | } 30 | }, [networks, setupNetwork, apiRegistry]); 31 | }; 32 | 33 | const useSyncAssets = (useTrigger: TriggerRefresh) => { 34 | const [refresh, _] = useTrigger; 35 | const { 36 | actions: { setLoading, setAssets }, 37 | state 38 | } = useContext(AppContext); 39 | const { networks, accounts, apiRegistry } = state; 40 | 41 | const prevRegistrySize = usePrevious(apiRegistry.size ?? 0); 42 | const prevAccountsSize = usePrevious(accounts.length ?? 0); 43 | 44 | const fetchAllAssets = useCallback( 45 | async ( 46 | networks: string[], 47 | accounts: IAccount[], 48 | apiRegistry: Map 49 | ): Promise => { 50 | setLoading(true, LoadingScope.assets); 51 | let assets: Asset[] = []; 52 | for (const networkWs of networks) { 53 | const api = apiRegistry.get(networkWs)!; 54 | (await Promise.allSettled(accounts.map(({ id: account }) => scrape(account, api)))).forEach( 55 | (result) => { 56 | if (result.status === 'fulfilled') { 57 | assets = assets.concat(result.value); 58 | } 59 | } 60 | ); 61 | } 62 | setLoading(false, LoadingScope.assets); 63 | return assets; 64 | }, 65 | [] 66 | ); 67 | 68 | const refreshAssets = useCallback( 69 | async (networks: string[], accounts: IAccount[], apiRegistry: Map) => { 70 | const assets = await fetchAllAssets(networks, accounts, apiRegistry); 71 | setAssets(assets); 72 | }, 73 | [fetchAllAssets, setAssets] 74 | ); 75 | 76 | useEffect(() => { 77 | if (apiRegistry.size !== prevRegistrySize || accounts.length !== prevAccountsSize) { 78 | refreshAssets(networks, accounts, apiRegistry); 79 | } 80 | }, [networks, accounts, apiRegistry, prevRegistrySize, prevAccountsSize, refresh]); 81 | }; 82 | 83 | type TriggerRefresh = [number, React.Dispatch>]; 84 | 85 | const useBackgroundFetch = () => { 86 | const triggerRefresh: TriggerRefresh = useState(0); 87 | useSyncNetworks(triggerRefresh); 88 | useSyncAssets(triggerRefresh); 89 | }; 90 | 91 | export default useBackgroundFetch; 92 | -------------------------------------------------------------------------------- /ui/src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function usePrevious(value: T): T { 4 | // The ref object is a generic container whose current property is mutable ... 5 | // ... and can hold any value, similar to an instance property on a class 6 | const ref: any = useRef(); 7 | // Store current value in ref 8 | useEffect(() => { 9 | ref.current = value; 10 | }, [value]); // Only re-run if value changes 11 | // Return previous value (happens before update in useEffect above) 12 | return ref.current; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 8 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { AppContextProvider } from './store'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /ui/src/store/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import { getLocalStorage, setLocalStorage } from '../utils/localStorage'; 3 | import AppReducer, { ActionTypes } from './reducer/AppReducer'; 4 | import { 5 | FAddAccount, 6 | FAddApiRegistry, 7 | FAddNetwork, 8 | FChangeVisibility, 9 | FRemoveApiRegistry, 10 | FRemoveNetwork, 11 | FSetAssets, 12 | FSetLoading, 13 | IActionList, 14 | IAppContext, 15 | IVisibility, 16 | StoreState 17 | } from './store'; 18 | 19 | const INITIAL_NETWORKS = [ 20 | 'wss://kusama-rpc.polkadot.io', 21 | 'wss://rpc.polkadot.io', 22 | 'wss://statemine-rpc.polkadot.io', 23 | 'wss://karura-rpc-0.aca-api.network', 24 | 'wss://acala-polkadot.api.onfinality.io/public-ws', 25 | 'wss://khala-api.phala.network/ws', 26 | 'wss://rpc.astar.network', 27 | 'wss://rpc.parallel.fi', 28 | 'wss://wss.api.moonbeam.network', 29 | 'wss://wss.moonriver.moonbeam.network' 30 | ]; 31 | 32 | const NETWORK_KEY = 'networks'; 33 | const ACCOUNT_KEY = 'accounts'; 34 | 35 | export const initialState = { 36 | accounts: getLocalStorage(ACCOUNT_KEY) ?? [], 37 | networks: getLocalStorage(NETWORK_KEY) ?? INITIAL_NETWORKS, 38 | assets: [], 39 | apiRegistry: new Map(), 40 | visibility: { 41 | networks: [], 42 | accounts: [] 43 | } as IVisibility, 44 | loading: false 45 | } as StoreState; 46 | 47 | export const defaultContext = { 48 | state: initialState, 49 | actions: {} as IActionList 50 | } as IAppContext; 51 | 52 | export const AppContext = React.createContext(defaultContext); 53 | 54 | export const outerAddApiRegistry: FAddApiRegistry = (network, registry) => { 55 | return { 56 | type: ActionTypes.AddRegistry, 57 | payload: { 58 | network, 59 | registry 60 | } 61 | }; 62 | }; 63 | 64 | export const AppContextProvider = ({ children }: { children: any }) => { 65 | const [state, dispatch]: [StoreState, any] = useReducer(AppReducer, initialState); 66 | 67 | const addNetwork: FAddNetwork = (network) => { 68 | dispatch({ 69 | type: ActionTypes.AddNetwork, 70 | payload: network 71 | }); 72 | }; 73 | 74 | const removeNetwork: FRemoveNetwork = (network) => { 75 | dispatch({ 76 | type: ActionTypes.RemoveNetwork, 77 | payload: network 78 | }); 79 | }; 80 | 81 | const addAccount: FAddAccount = (account) => { 82 | dispatch({ 83 | type: ActionTypes.AddAccount, 84 | payload: account 85 | }); 86 | }; 87 | 88 | const removeAccount: FRemoveNetwork = (accountId) => { 89 | dispatch({ 90 | type: ActionTypes.RemoveAccount, 91 | payload: accountId 92 | }); 93 | }; 94 | 95 | const setLoading: FSetLoading = (loadingState, scope) => { 96 | dispatch({ 97 | type: ActionTypes.SetLoadingState, 98 | payload: { 99 | loadingState, 100 | scope 101 | } 102 | }); 103 | }; 104 | 105 | const addApiRegistry: FAddApiRegistry = (network, registry) => { 106 | dispatch({ 107 | type: ActionTypes.AddRegistry, 108 | payload: { 109 | network, 110 | registry 111 | } 112 | }); 113 | }; 114 | 115 | const removeApiRegistry: FRemoveApiRegistry = (network) => { 116 | dispatch({ 117 | type: ActionTypes.RemoveRegistry, 118 | payload: network 119 | }); 120 | }; 121 | 122 | const setAssets: FSetAssets = (assets) => { 123 | dispatch({ 124 | type: ActionTypes.SetAssets, 125 | payload: assets 126 | }); 127 | }; 128 | 129 | const changeVisibility: FChangeVisibility = (account = null, network = null) => { 130 | dispatch({ 131 | type: ActionTypes.ChangeVisibility, 132 | payload: { 133 | account, 134 | network 135 | } 136 | }); 137 | }; 138 | 139 | const actions = { 140 | addNetwork, 141 | removeNetwork, 142 | addAccount, 143 | removeAccount, 144 | setLoading, 145 | addApiRegistry, 146 | removeApiRegistry, 147 | setAssets, 148 | changeVisibility 149 | } as IActionList; 150 | 151 | const context = { 152 | state, 153 | actions 154 | }; 155 | 156 | React.useEffect(() => { 157 | const { accounts, networks } = state; 158 | setLocalStorage(ACCOUNT_KEY, accounts); 159 | setLocalStorage(NETWORK_KEY, networks); 160 | }, [state]); 161 | 162 | return {children}; 163 | }; 164 | -------------------------------------------------------------------------------- /ui/src/store/reducer/AppReducer.ts: -------------------------------------------------------------------------------- 1 | import { IAccount, LoadingScope, StoreState } from '../store'; 2 | import { Asset, ApiPromise } from 'polkadot-portfolio-core'; 3 | 4 | export enum ActionTypes { 5 | AddNetwork = 'AddNetwork', 6 | RemoveNetwork = 'RemoveNetwork', 7 | AddAccount = 'AddAccount', 8 | RemoveAccount = 'RemoveAccount', 9 | SetLoadingState = 'SetLoadingState', 10 | AddRegistry = 'AddRegistry', 11 | RemoveRegistry = 'RemoveRegistry', 12 | SetAssets = 'SetAssets', 13 | ChangeVisibility = 'ChangeVisibility' 14 | } 15 | 16 | export interface IAction { 17 | type: ActionTypes; 18 | payload: any; 19 | } 20 | 21 | const addAccount = (state: StoreState, payload: IAccount): StoreState => { 22 | if (state.accounts.some((item) => item.id === payload.id)) return state; 23 | return { 24 | ...state, 25 | accounts: [...state.accounts, payload] 26 | }; 27 | }; 28 | 29 | const removeAccount = (state: StoreState, accountId: string): StoreState => { 30 | const { 31 | visibility: { accounts } 32 | } = state; 33 | const updatedAccounts = accounts.includes(accountId) 34 | ? accounts.filter((item) => item !== accountId) 35 | : accounts; 36 | return { 37 | ...state, 38 | accounts: state.accounts.filter((item) => item.id !== accountId), 39 | visibility: { 40 | ...state.visibility, 41 | accounts: updatedAccounts 42 | } 43 | }; 44 | }; 45 | 46 | const addNetwork = (state: StoreState, network: string): StoreState => { 47 | if (state.networks.includes(network)) return state; 48 | return { 49 | ...state, 50 | networks: [...state.networks, network] 51 | }; 52 | }; 53 | 54 | const removeNetwork = (state: StoreState, network: string): StoreState => ({ 55 | ...state, 56 | networks: state.networks.filter((item) => item !== network) 57 | }); 58 | 59 | const setLoadingState = ( 60 | state: StoreState, 61 | loadingState: boolean, 62 | scope: LoadingScope 63 | ): StoreState => ({ 64 | ...state, 65 | loading: { 66 | ...state.loading, 67 | [scope]: loadingState 68 | } 69 | }); 70 | 71 | const addRegistry = (state: StoreState, network: string, registry: ApiPromise): StoreState => ({ 72 | ...state, 73 | apiRegistry: state.apiRegistry.set(network, registry) 74 | }); 75 | 76 | const removeRegistry = (state: StoreState, network: string): StoreState => { 77 | state.apiRegistry.delete(network); 78 | const { 79 | visibility: { networks } 80 | } = state; 81 | const updatedNetworks = networks.includes(network) 82 | ? networks.filter((item) => item !== network) 83 | : networks; 84 | return { 85 | ...state, 86 | visibility: { 87 | ...state.visibility, 88 | networks: updatedNetworks 89 | } 90 | }; 91 | }; 92 | 93 | const setAssets = (state: StoreState, assets: Asset[]): StoreState => ({ 94 | ...state, 95 | assets 96 | }); 97 | 98 | const changeVisibility = ( 99 | state: StoreState, 100 | network: string | null, 101 | account: string | null 102 | ): StoreState => { 103 | const { visibility } = state; 104 | const { networks, accounts } = visibility; 105 | let updatedNetworks: string[] = networks, 106 | updatedAccounts: string[] = accounts; 107 | if (network) { 108 | const hasNetwork = networks.includes(network); 109 | updatedNetworks = hasNetwork 110 | ? networks.filter((item) => item !== network) 111 | : [...networks, network]; 112 | } 113 | if (account) { 114 | const hasAccount = accounts.includes(account); 115 | updatedAccounts = hasAccount 116 | ? accounts.filter((item) => item !== account) 117 | : [...accounts, account]; 118 | } 119 | return { 120 | ...state, 121 | visibility: { 122 | networks: updatedNetworks, 123 | accounts: updatedAccounts 124 | } 125 | }; 126 | }; 127 | 128 | const AppReducer = (state: StoreState, action: IAction): StoreState => { 129 | switch (action.type) { 130 | case ActionTypes.AddAccount: 131 | return addAccount(state, action.payload); 132 | case ActionTypes.RemoveAccount: 133 | return removeAccount(state, action.payload); 134 | case ActionTypes.AddNetwork: 135 | return addNetwork(state, action.payload); 136 | case ActionTypes.RemoveNetwork: 137 | return removeNetwork(state, action.payload); 138 | case ActionTypes.SetLoadingState: 139 | return setLoadingState(state, action.payload.loadingState, action.payload.scope); 140 | case ActionTypes.AddRegistry: 141 | return addRegistry(state, action.payload.network, action.payload.registry); 142 | case ActionTypes.RemoveRegistry: 143 | return removeRegistry(state, action.payload); 144 | case ActionTypes.SetAssets: 145 | return setAssets(state, action.payload); 146 | case ActionTypes.ChangeVisibility: 147 | return changeVisibility(state, action.payload.network, action.payload.account); 148 | default: 149 | return state; 150 | } 151 | }; 152 | 153 | export default AppReducer; 154 | -------------------------------------------------------------------------------- /ui/src/store/store.d.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from './types/Asset'; 2 | import { IApiRegistry, ApiPromise } from 'polkadot-portfolio-core'; 3 | 4 | export interface StoreState { 5 | accounts: IAccount[]; 6 | networks: string[]; 7 | apiRegistry: IApiRegistry; 8 | assets: Asset[]; 9 | visibility: IVisibility; 10 | loading: LoadingStates; 11 | } 12 | 13 | export interface IVisibility { 14 | networks: string[]; 15 | accounts: string[]; 16 | } 17 | 18 | export interface IAppContext { 19 | state: StoreState; 20 | actions: IActionList; 21 | } 22 | 23 | export interface IAccount { 24 | name: string; 25 | id: string; 26 | } 27 | 28 | export interface LoadingStates { 29 | [key: LoadingScope]: boolean; 30 | } 31 | 32 | export enum LoadingScope { 33 | networks = 'networks', 34 | assets = 'assets' 35 | } 36 | 37 | export interface IActionList { 38 | addNetwork: FAddNetwork; 39 | removeNetwork: FRemoveNetwork; 40 | addAccount: FAddAccount; 41 | removeAccount: FRemoveAccount; 42 | setLoading: FSetLoading; 43 | addApiRegistry: FAddApiRegistry; 44 | removeApiRegistry: FRemoveApiRegistry; 45 | setAssets: FSetAssets; 46 | changeVisibility: FChangeVisibility; 47 | } 48 | 49 | export type FAddNetwork = (network: string) => void; 50 | export type FRemoveNetwork = (network: string) => void; 51 | export type FAddAccount = (account: IAccount) => void; 52 | export type FRemoveAccount = (accountId: string) => void; 53 | export type FSetLoading = (state: boolean, scope: LoadingScope) => void; 54 | export type FAddApiRegistry = (network: string, registry: ApiPromise) => void; 55 | export type FRemoveApiRegistry = (network: string) => void; 56 | export type FSetAssets = (assets: Asset[]) => void; 57 | export type FChangeVisibility = (account?: string | null, network?: string | null) => void; 58 | -------------------------------------------------------------------------------- /ui/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export enum AssetGroups { 2 | Name, 3 | Token, 4 | Account, 5 | Chain, 6 | Source, 7 | Amount, 8 | Value 9 | } 10 | 11 | interface TTableHeads { 12 | title: string; 13 | assetGroup: AssetGroups; 14 | } 15 | 16 | export const tableHeads: TTableHeads[] = [ 17 | { 18 | title: 'Name', 19 | assetGroup: AssetGroups.Name 20 | }, 21 | { 22 | title: 'Token', 23 | assetGroup: AssetGroups.Token 24 | }, 25 | { 26 | title: 'Account', 27 | assetGroup: AssetGroups.Account 28 | }, 29 | { 30 | title: 'Chain', 31 | assetGroup: AssetGroups.Chain 32 | }, 33 | { 34 | title: 'Source', 35 | assetGroup: AssetGroups.Source 36 | }, 37 | { 38 | title: 'Amount', 39 | assetGroup: AssetGroups.Amount 40 | }, 41 | { 42 | title: 'Value', 43 | assetGroup: AssetGroups.Value 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /ui/src/utils/localStorage/index.ts: -------------------------------------------------------------------------------- 1 | export const setLocalStorage = (key: string, value: any) => { 2 | (window as any).localStorage.setItem(key, JSON.stringify(value)); 3 | }; 4 | 5 | export const getLocalStorage = (key: string): any => { 6 | const value = (window as any).localStorage.getItem(key); 7 | return JSON.parse(value); 8 | }; 9 | -------------------------------------------------------------------------------- /ui/src/utils/styles.ts: -------------------------------------------------------------------------------- 1 | export const SharedStyles = { 2 | button: { 3 | default: 4 | 'rounded-md mt-7 bg-green-500 hover:bg-green-700 text-center py-2 px-4 mt-2 w-full appearance-none text-white', 5 | disabled: 'bg-gray-300 hover:bg-gray-300 cursor-not-allowed' 6 | }, 7 | input: { 8 | default: 'border rounded-md border-gray-100 bg-white w-full py-2 px-4', 9 | invalid: 'border-red-500 text-red-500' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /ui/src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | export const validateWebsocketUrls = (input: string): boolean => { 2 | // Got (and modified a wee bit) the regex from here: https://stackoverflow.com/questions/57288837/regex-if-valid-websocket-address 3 | const regex = 4 | /^(wss?:\/\/)([0-9]{1,3}(?:\.[0-9]{1,3}){3}|(?=[^/]{1,254}(?![^/]))(?:(?=[a-zA-Z0-9-]{1,63}\.)(?:xn--+)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}):?([0-9]{1,5})?\/*(?:[\w\d-/])*$/; 5 | return regex.test(input); 6 | }; 7 | -------------------------------------------------------------------------------- /ui/src/views/Dashboard/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Header from '../../components/Header'; 2 | import Networks from '../../components/Networks'; 3 | import { Accounts } from '../../components/Accounts'; 4 | import About from '../../components/About'; 5 | import { RefObject, useEffect, useRef, useState } from 'react'; 6 | 7 | const Sidebar = () => { 8 | const headerHeight: RefObject = useRef(null); 9 | const aboutHeight: RefObject = useRef(null); 10 | const [hHeight, setHeaderHeight] = useState(58); 11 | const [aHeight, setAboutHeight] = useState(58); 12 | 13 | useEffect(() => { 14 | headerHeight.current && setHeaderHeight(headerHeight.current?.clientHeight); 15 | aboutHeight.current && setAboutHeight(aboutHeight.current?.clientHeight); 16 | }, [headerHeight, aboutHeight]); 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Sidebar; 40 | -------------------------------------------------------------------------------- /ui/src/views/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Assets from '../../components/Assets'; 3 | import Sidebar from './Sidebar'; 4 | 5 | const Dashboard = () => { 6 | return ( 7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Dashboard; 19 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 3 | theme: { 4 | extend: {} 5 | }, 6 | plugins: [] 7 | }; 8 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------