├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── forge.config.ts ├── package.json ├── postcss.config.js ├── src ├── assets │ └── icons │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ └── source.png ├── bin │ ├── darwin │ │ ├── algod │ │ ├── goal │ │ └── kmd │ ├── linux │ │ ├── algod │ │ ├── goal │ │ └── kmd │ └── win32 │ │ ├── algod.exe │ │ ├── goal.exe │ │ └── kmd.exe ├── bridge │ └── goal.ts ├── config │ ├── algorand.mainnet.config.json │ ├── algorand.mainnet.genesis.json │ ├── voi.mainnet.config.json │ └── voi.mainnet.genesis.json ├── main.ts ├── preload.ts ├── render │ ├── App.tsx │ ├── components │ │ ├── app │ │ │ ├── Body │ │ │ │ ├── Column.tsx │ │ │ │ ├── Dashboard │ │ │ │ │ ├── AccountSelector.tsx │ │ │ │ │ ├── AccountViewer.tsx │ │ │ │ │ ├── StatNumber.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Flush.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ ├── StatusIndicator.tsx │ │ │ │ ├── StepViewer.tsx │ │ │ │ └── index.tsx │ │ │ └── Header │ │ │ │ ├── DarkMode.tsx │ │ │ │ └── index.tsx │ │ ├── icons │ │ │ ├── Antenna.tsx │ │ │ ├── Check.tsx │ │ │ ├── Circle.tsx │ │ │ ├── Clipboard.tsx │ │ │ ├── Eye.tsx │ │ │ ├── Gear.tsx │ │ │ ├── HotAirBalloon.tsx │ │ │ ├── Key.tsx │ │ │ ├── Minus.tsx │ │ │ ├── Moon.tsx │ │ │ ├── OpenNewWindow.tsx │ │ │ ├── Square.tsx │ │ │ ├── Sun.tsx │ │ │ ├── Undo.tsx │ │ │ ├── Wallet.tsx │ │ │ └── X.tsx │ │ ├── modals │ │ │ └── Modal.tsx │ │ └── shared │ │ │ ├── Button.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── Console.tsx │ │ │ ├── CopySnippet.tsx │ │ │ ├── Error.tsx │ │ │ ├── Link.tsx │ │ │ ├── Select.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Success.tsx │ │ │ ├── TextInput.tsx │ │ │ └── Tooltip.tsx │ ├── flux │ │ ├── accountsStore.ts │ │ ├── index.ts │ │ └── wizardStore.ts │ ├── index.css │ ├── index.html │ └── utils.ts └── renderer.ts ├── tailwind.config.js ├── tsconfig.json ├── webpack.main.config.ts ├── webpack.plugins.ts ├── webpack.renderer.config.ts ├── webpack.rules.ts └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/electron", 13 | "plugin:import/typescript" 14 | ], 15 | "parser": "@typescript-eslint/parser" 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/bin/darwin/* filter=lfs diff=lfs merge=lfs -text 2 | src/bin/linux/* filter=lfs diff=lfs merge=lfs -text 3 | src/bin/win32/* filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Vite 89 | .vite/ 90 | 91 | # Electron-Forge 92 | out/ 93 | 94 | # packaged binaries 95 | src/bin/packaged/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Austen Probst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aust's One-Click Node 2 | 3 | ## Sunsetting 4 | 5 | > I built A1CN ~2 years ago for free and open-sourced it because there was a need in the community. Now that governance is over, and nodekit keeps improving, that need is no longer there. 6 | > 7 | > Therefore, I'm officially sunsetting A1CN. 8 | > 9 | > For those currently using the app: you don't need to take immediate action. The app will continue to work for some time. However, when a new consensus upgrade comes out (i.e. p2p), then A1CN will no longer work. So whenever you have a spare weekend, you should migrate to nodekit. 10 | > 11 | > Thanks for the memories. 12 | > 13 | > April 2nd, 2025 14 | 15 | https://x.com/austp17/status/1907481612885086553 16 | 17 | Aust's One-Click Node is an app for Mac, Windows, and Linux that makes it easy to spin up a node for Algorand or Voi and start participating in consensus. 18 | 19 | Screen Shot 2023-08-03 at 9 08 44 AM 20 | 21 | ## Installation 22 | 23 | Head over to the [releases page](https://github.com/AustP/austs-one-click-node/releases) and download the file required by your operating system. 24 | 25 | Mac builds are signed and notarized, so you shouldn't have any issue running them. 26 | 27 | Windows builds are not signed because $500 is too much. 28 | In order to run it, you'll need to click "More info" on the "Windows protected your PC" dialog. 29 | Then click the "Run Anyway" button. 30 | The code is open-source so you can review it yourself or have a trusted friend do so. 31 | 32 | ## Running Multiple Nodes 33 | 34 | If you want to run a node for Algorand and Voi, you can launch another node from the Settings page. There is one quirk to be aware of: 35 | 36 | The list of accounts is shared between the two instances. 37 | 38 | What this means is that you will need to have your Algorand accounts and Voi accounts shared in the account list. The best way to do this is by adding all of your accounts as watch accounts. When you need to sign transactions, then you should connect your wallet and sign the transactions. When you're done, disconnect the wallet so it becomes a watch account again. If you follow this protocol, you should have no problems with this quirk. 39 | 40 | ## Signing Transactions With a Voi Account 41 | 42 | Defly doesn't work with Voi. However, if you use [A-Wallet](https://a-wallet.net/) (an [open-source project](https://github.com/scholtz/wallet/), use at your own risk!), you can sign transactions. Here is the process: 43 | 44 | 1. Click the "Connect" button for Defly. 45 | 2. When the QR Code pops up, click it to copy the Wallet Connect URL. 46 | 3. In A-Wallet, go to the WalletConnect tab and click the "Initialize connection to Wallet Connect" button. 47 | 4. Paste the URL into the textbox and press the "Connect" button. 48 | 49 | Now when you press the Go Online/Offline buttons, A-Wallet should prompt you to sign those transactions. 50 | 51 | Thanks to APT, pk, and django.algo in Discord for sharing these instructions! 52 | -------------------------------------------------------------------------------- /forge.config.ts: -------------------------------------------------------------------------------- 1 | import { MakerDMG } from '@electron-forge/maker-dmg'; 2 | import { MakerSquirrel } from '@electron-forge/maker-squirrel'; 3 | import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; 4 | import { WebpackPlugin } from '@electron-forge/plugin-webpack'; 5 | import type { ForgeConfig } from '@electron-forge/shared-types'; 6 | import 'dotenv/config'; 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | 10 | import packageConfig from './package.json'; 11 | import { mainConfig } from './webpack.main.config'; 12 | import { rendererConfig } from './webpack.renderer.config'; 13 | 14 | const ASSETS_DIR = path.join(__dirname, 'src', 'assets'); 15 | 16 | const config: ForgeConfig = { 17 | packagerConfig: { 18 | asar: true, 19 | afterCopyExtraResources: [ 20 | (buildPath, electronVersion, platform, arch, callback) => { 21 | if (platform === 'win32') { 22 | fs.renameSync( 23 | `${buildPath}/resources/algod`, 24 | `${buildPath}/resources/algod.exe`, 25 | ); 26 | fs.renameSync( 27 | `${buildPath}/resources/goal`, 28 | `${buildPath}/resources/goal.exe`, 29 | ); 30 | fs.renameSync( 31 | `${buildPath}/resources/kmd`, 32 | `${buildPath}/resources/kmd.exe`, 33 | ); 34 | } 35 | 36 | callback(); 37 | }, 38 | ], 39 | beforeCopyExtraResources: [ 40 | (buildPath, electronVersion, platform, arch, callback) => { 41 | const suffix = platform === 'win32' ? '.exe' : ''; 42 | const algod = `algod${suffix}`; 43 | const goal = `goal${suffix}`; 44 | const kmd = `kmd${suffix}`; 45 | 46 | try { 47 | // delete the packaged directory if it exists 48 | if (fs.existsSync('src/bin/packaged')) { 49 | fs.rmdirSync('src/bin/packaged', { recursive: true }); 50 | } 51 | 52 | // copy the platform binaries to the packaged directory 53 | fs.mkdirSync('src/bin/packaged'); 54 | fs.copyFileSync( 55 | `src/bin/${platform}/${algod}`, 56 | `src/bin/packaged/algod`, 57 | ); 58 | fs.copyFileSync( 59 | `src/bin/${platform}/${goal}`, 60 | `src/bin/packaged/goal`, 61 | ); 62 | fs.copyFileSync(`src/bin/${platform}/${kmd}`, `src/bin/packaged/kmd`); 63 | 64 | callback(); 65 | } catch (err) { 66 | callback(err); 67 | } 68 | }, 69 | ], 70 | executableName: packageConfig.name, 71 | extraResource: [ 72 | 'src/assets/icons/icon.icns', 73 | 'src/assets/icons/icon.ico', 74 | 'src/assets/icons/icon.png', 75 | 'src/bin/packaged/algod', 76 | 'src/bin/packaged/goal', 77 | 'src/bin/packaged/kmd', 78 | 'src/config/algorand.mainnet.genesis.json', 79 | 'src/config/algorand.mainnet.config.json', 80 | 'src/config/voi.mainnet.genesis.json', 81 | 'src/config/voi.mainnet.config.json', 82 | ], 83 | icon: path.join(ASSETS_DIR, 'icons', 'icon'), 84 | osxNotarize: { 85 | appleId: process.env.APPLE_ID!, 86 | appleIdPassword: process.env.APPLE_PASSWORD!, 87 | teamId: process.env.APPLE_TEAM_ID!, 88 | tool: 'notarytool', 89 | }, 90 | osxSign: {}, 91 | }, 92 | rebuildConfig: {}, 93 | makers: [ 94 | // MakerDeb class was having issues, so use raw object format 95 | { 96 | name: '@electron-forge/maker-deb', 97 | config: { 98 | icon: path.join(ASSETS_DIR, 'icons', 'icon.png'), 99 | }, 100 | }, 101 | new MakerDMG({ 102 | icon: path.join(ASSETS_DIR, 'icons', 'icon.icns'), 103 | name: 'Austs One-Click Node', 104 | overwrite: true, 105 | }), 106 | new MakerSquirrel({ 107 | exe: 'austs-one-click-node.exe', 108 | iconUrl: 109 | 'https://raw.githubusercontent.com/AustP/austs-one-click-node/main/assets/icons/win/icon.ico', 110 | setupExe: 'austs-one-click-node-setup.exe', 111 | setupIcon: path.join(ASSETS_DIR, 'icons', 'icon.ico'), 112 | }), 113 | ], 114 | plugins: [ 115 | new AutoUnpackNativesPlugin({}), 116 | new WebpackPlugin({ 117 | mainConfig, 118 | renderer: { 119 | config: rendererConfig, 120 | entryPoints: [ 121 | { 122 | html: './src/render/index.html', 123 | js: './src/renderer.ts', 124 | name: 'main_window', 125 | preload: { 126 | js: './src/preload.ts', 127 | }, 128 | }, 129 | ], 130 | }, 131 | }), 132 | ], 133 | publishers: [ 134 | { 135 | name: '@electron-forge/publisher-github', 136 | config: { 137 | draft: true, 138 | repository: { 139 | authToken: process.env.GITHUB_TOKEN, 140 | name: 'austs-one-click-node', 141 | owner: 'AustP', 142 | }, 143 | }, 144 | }, 145 | ], 146 | }; 147 | 148 | export default config; 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "austs-one-click-node", 3 | "productName": "Austs One-Click Node", 4 | "version": "1.6.1", 5 | "author": "Aust", 6 | "description": "An easy to use interface to run an Algorand or Voi consensus node.", 7 | "main": ".webpack/main", 8 | "scripts": { 9 | "create-icons": "electron-icon-builder --input=./src/assets/icons/source.png --output=./src/assets", 10 | "lint": "eslint --ext .ts,.tsx .", 11 | "make:linux": "electron-forge make --platform linux", 12 | "make:mac": "electron-forge make --platform darwin", 13 | "make:win": "electron-forge make --platform win32", 14 | "publish:linux": "electron-forge publish --platform linux", 15 | "publish:mac": "electron-forge publish --platform darwin", 16 | "publish:win": "electron-forge publish --platform win32", 17 | "start": "electron-forge start --inspect-electron", 18 | "update-libs": "yarn upgrade algoseas-libs" 19 | }, 20 | "keywords": [], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/core": "^7.22.5", 24 | "@babel/preset-react": "^7.22.5", 25 | "@electron-forge/cli": "^6.2.1", 26 | "@electron-forge/maker-deb": "^6.2.1", 27 | "@electron-forge/maker-dmg": "^6.2.1", 28 | "@electron-forge/maker-squirrel": "^6.2.1", 29 | "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", 30 | "@electron-forge/plugin-webpack": "^6.2.1", 31 | "@electron-forge/publisher-github": "^6.2.1", 32 | "@types/node": "^20.3.3", 33 | "@types/react": "^18.2.14", 34 | "@types/react-dom": "^18.2.6", 35 | "@typescript-eslint/eslint-plugin": "^5.0.0", 36 | "@typescript-eslint/parser": "^5.0.0", 37 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 38 | "autoprefixer": "^10.4.14", 39 | "babel-loader": "^9.1.2", 40 | "css-loader": "^6.0.0", 41 | "dotenv": "^16.3.1", 42 | "electron": "25.2.0", 43 | "electron-icon-builder": "^2.0.1", 44 | "eslint": "^8.0.1", 45 | "eslint-plugin-import": "^2.25.0", 46 | "fork-ts-checker-webpack-plugin": "^7.2.13", 47 | "isomorphic-ws": "^5.0.0", 48 | "node-loader": "^2.0.0", 49 | "node-polyfill-webpack-plugin": "^2.0.1", 50 | "postcss": "^8.4.24", 51 | "postcss-font-magician": "^3.0.0", 52 | "postcss-loader": "^7.3.3", 53 | "prettier": "^2.8.8", 54 | "style-loader": "^3.0.0", 55 | "tailwindcss": "^3.3.2", 56 | "ts-loader": "^9.2.2", 57 | "ts-node": "^10.0.0", 58 | "tsconfig-paths-webpack-plugin": "^4.0.1", 59 | "typescript": "~4.5.4" 60 | }, 61 | "dependencies": { 62 | "@aust/react-flux": "^1.3.1", 63 | "@blockshake/defly-connect": "^1.1.5", 64 | "@perawallet/connect": "1.3.4", 65 | "@txnlab/use-wallet": "^2.0.0", 66 | "algoseas-libs": "git+https://oauth2:5M-yAwqQJ_U-R-d7Pp9x@gitlab.probsttech.com/algoseas/libs.git", 67 | "electron-is-dev": "^2.0.0", 68 | "electron-persist-secure": "^1.3.0", 69 | "electron-squirrel-startup": "^1.0.0", 70 | "immer": "^10.0.2", 71 | "react": "^18.2.0", 72 | "react-dom": "^18.2.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('postcss-font-magician')({ 5 | foundries: ['google'], 6 | protocol: 'https:', 7 | variants: { 8 | Montserrat: { 9 | 200: [], 10 | 300: [], 11 | 400: [], 12 | '400 italic': [], 13 | 500: [], 14 | }, 15 | }, 16 | }), 17 | require('autoprefixer'), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustP/austs-one-click-node/6f80984f90d52429f45f8868044f42c10f3c275b/src/assets/icons/icon.icns -------------------------------------------------------------------------------- /src/assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustP/austs-one-click-node/6f80984f90d52429f45f8868044f42c10f3c275b/src/assets/icons/icon.ico -------------------------------------------------------------------------------- /src/assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustP/austs-one-click-node/6f80984f90d52429f45f8868044f42c10f3c275b/src/assets/icons/icon.png -------------------------------------------------------------------------------- /src/assets/icons/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AustP/austs-one-click-node/6f80984f90d52429f45f8868044f42c10f3c275b/src/assets/icons/source.png -------------------------------------------------------------------------------- /src/bin/darwin/algod: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d6a7a87916d3f6561e8254d8b4758a715ef947c9801ba9fe95d7a16b333f9754 3 | size 85237168 4 | -------------------------------------------------------------------------------- /src/bin/darwin/goal: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ad326ebcf6fc8f0ace5f4ab292f4c1b2cab7008eafd69b0a30408647c7ee86d0 3 | size 73089824 4 | -------------------------------------------------------------------------------- /src/bin/darwin/kmd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f72e2b0d1d202e82cebd012feb80f1c0220cb708874c73873a6ca57f13ca1204 3 | size 38195824 4 | -------------------------------------------------------------------------------- /src/bin/linux/algod: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:539c210f6b79129bbc104bf5b648357e1c2b82aab9939acada7ba079abb3bb1d 3 | size 85304616 4 | -------------------------------------------------------------------------------- /src/bin/linux/goal: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4610690fb6eafed5eac1f47c23da56ca9661f9f710f063d661ac0083e0d7d7e9 3 | size 73232816 4 | -------------------------------------------------------------------------------- /src/bin/linux/kmd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1c97a9d1ae114b9dbf9e74a6bd0649e6dc8fe3e12c696a32462f787c8ed078d4 3 | size 38450808 4 | -------------------------------------------------------------------------------- /src/bin/win32/algod.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a34d018a1c31af42e29b5194d8e91c24e41b658cd3e4bf4d30936c6a270c79b8 3 | size 139703162 4 | -------------------------------------------------------------------------------- /src/bin/win32/goal.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7c8e030917b863f2d929be3cc8e56e02010e737105af01fd4140eb0f8283e0d6 3 | size 122465279 4 | -------------------------------------------------------------------------------- /src/bin/win32/kmd.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:efa0aeb350f1340588968ace2468abe359ba62e24ef6e9591bcdb50eeb9efeaf 3 | size 65664629 4 | -------------------------------------------------------------------------------- /src/bridge/goal.ts: -------------------------------------------------------------------------------- 1 | import { exec, spawn } from 'child_process'; 2 | import { app, BrowserWindow, ipcMain } from 'electron'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import { ModifiedBrowserWindow } from '../main'; 7 | 8 | const CATCHPOINT_ENDPOINTS = { 9 | 'algorand.mainnet': 10 | 'https://algorand-catchpoints.s3.us-east-2.amazonaws.com/channel/mainnet/latest.catchpoint', 11 | 'voi.mainnet': 'https://mainnet-api.voi.nodely.dev/v2/status', 12 | }; 13 | 14 | const CONFIG_FILES = { 15 | 'algorand.mainnet': 'algorand.mainnet.config.json', 16 | 'voi.mainnet': 'voi.mainnet.config.json', 17 | }; 18 | 19 | const GENESIS_FILES = { 20 | 'algorand.mainnet': 'algorand.mainnet.genesis.json', 21 | 'voi.mainnet': 'voi.mainnet.genesis.json', 22 | }; 23 | 24 | const SUFFIX = process.platform === 'win32' ? '.exe' : ''; 25 | const ALGOD = app.isPackaged 26 | ? path.join(process.resourcesPath, `algod${SUFFIX}`) 27 | : path.join( 28 | __dirname, // one-click-node/.webpack/main 29 | '..', 30 | '..', 31 | 'src', 32 | 'bin', 33 | process.platform, 34 | `algod${SUFFIX}`, 35 | ); 36 | const GOAL = app.isPackaged 37 | ? path.join(process.resourcesPath, `goal${SUFFIX}`) 38 | : path.join( 39 | __dirname, // one-click-node/.webpack/main 40 | '..', 41 | '..', 42 | 'src', 43 | 'bin', 44 | process.platform, 45 | `goal${SUFFIX}`, 46 | ); 47 | 48 | const CONFIG_DIR = app.isPackaged 49 | ? process.resourcesPath 50 | : path.join( 51 | __dirname, // one-click-node/.webpack/main 52 | '..', 53 | '..', 54 | 'src', 55 | 'config', 56 | ); 57 | 58 | ipcMain.on('goal.addpartkey', (event, { account, firstValid, lastValid }) => { 59 | const window = BrowserWindow.fromWebContents( 60 | event.sender, 61 | )! as ModifiedBrowserWindow; 62 | 63 | const child = spawn(GOAL, [ 64 | 'account', 65 | 'addpartkey', 66 | '-d', 67 | window.getDataDir(), 68 | '-a', 69 | account, 70 | '--roundFirstValid', 71 | firstValid, 72 | '--roundLastValid', 73 | lastValid, 74 | ]); 75 | 76 | child.stderr.on('data', (data: Uint8Array) => 77 | window.webContents.send( 78 | 'goal.addpartkey.stderr', 79 | String.fromCharCode.apply(null, data), 80 | ), 81 | ); 82 | child.stdout.on('data', (data: Uint8Array) => 83 | window.webContents.send( 84 | 'goal.addpartkey.stdout', 85 | null, 86 | String.fromCharCode.apply(null, data), 87 | ), 88 | ); 89 | 90 | child.on('exit', (code) => 91 | window.webContents.send('goal.addpartkey', code ? true : null), 92 | ); 93 | }); 94 | 95 | ipcMain.on('goal.catchpoint', async (event) => { 96 | const window = BrowserWindow.fromWebContents( 97 | event.sender, 98 | )! as ModifiedBrowserWindow; 99 | 100 | let err = null; 101 | let stdout = null; 102 | 103 | const network = window.network; 104 | try { 105 | if (network === 'voi.mainnet') { 106 | const response = await fetch( 107 | CATCHPOINT_ENDPOINTS[network as keyof typeof CATCHPOINT_ENDPOINTS], 108 | ); 109 | const json = await response.json(); 110 | stdout = json['last-catchpoint']; 111 | } else { 112 | const response = await fetch( 113 | CATCHPOINT_ENDPOINTS[network as keyof typeof CATCHPOINT_ENDPOINTS], 114 | ); 115 | stdout = await response.text(); 116 | } 117 | } catch (e) { 118 | err = e; 119 | } 120 | 121 | window.webContents.send('goal.catchpoint', err, stdout); 122 | }); 123 | 124 | ipcMain.on('goal.catchup', (event, { catchpoint }) => { 125 | const window = BrowserWindow.fromWebContents( 126 | event.sender, 127 | )! as ModifiedBrowserWindow; 128 | exec( 129 | `"${GOAL}" node catchup -d "${window.getDataDir()}" ${catchpoint}`, 130 | (err, stdout) => window.webContents.send('goal.catchup', err, stdout), 131 | ); 132 | }); 133 | 134 | ipcMain.on('goal.deletepartkey', (event, { id }) => { 135 | const window = BrowserWindow.fromWebContents( 136 | event.sender, 137 | )! as ModifiedBrowserWindow; 138 | exec( 139 | `"${GOAL}" account deletepartkey -d "${window.getDataDir()}" --partkeyid ${id}`, 140 | (err, stdout) => window.webContents.send('goal.deletepartkey', err, stdout), 141 | ); 142 | }); 143 | 144 | ipcMain.on('goal.running', async (event) => { 145 | const window = BrowserWindow.fromWebContents( 146 | event.sender, 147 | )! as ModifiedBrowserWindow; 148 | exec(`"${GOAL}" node status -d "${window.getDataDir()}"`, (err, stdout) => 149 | window.webContents.send( 150 | 'goal.running', 151 | null, 152 | stdout.includes('Last committed block:'), 153 | ), 154 | ); 155 | }); 156 | 157 | let runningDataDirs = new Set(); 158 | ipcMain.on('goal.start', (event) => { 159 | const window = BrowserWindow.fromWebContents( 160 | event.sender, 161 | )! as ModifiedBrowserWindow; 162 | const network = window.network; 163 | const dataDir = window.getDataDir(); 164 | 165 | if (!fs.existsSync(path.join(dataDir, 'config.json'))) { 166 | fs.copyFileSync( 167 | path.join(CONFIG_DIR, CONFIG_FILES[network as keyof typeof CONFIG_FILES]), 168 | path.join(dataDir, 'config.json'), 169 | ); 170 | } 171 | 172 | if (!fs.existsSync(path.join(dataDir, 'genesis.json'))) { 173 | fs.copyFileSync( 174 | path.join( 175 | CONFIG_DIR, 176 | GENESIS_FILES[network as keyof typeof GENESIS_FILES], 177 | ), 178 | path.join(dataDir, 'genesis.json'), 179 | ); 180 | } 181 | 182 | const child = spawn(ALGOD, [ 183 | '-d', 184 | dataDir, 185 | '-l', 186 | `0.0.0.0:${window.store.get('port')}`, 187 | ]); 188 | window.on('closed', () => exec(`"${GOAL}" node stop -d "${dataDir}"`)); 189 | 190 | child.stderr.on('data', (data: Uint8Array) => 191 | window.webContents.send( 192 | 'goal.start', 193 | String.fromCharCode.apply(null, data), 194 | ), 195 | ); 196 | 197 | child.stdout.on('data', (data: Uint8Array) => { 198 | const str = String.fromCharCode.apply(null, data); 199 | if (str.includes('Node running')) { 200 | window.webContents.send('goal.start', null, str); 201 | } else if (str.includes('Could not start node')) { 202 | window.webContents.send('goal.start', new Error(str)); 203 | } 204 | }); 205 | 206 | child.on('exit', () => runningDataDirs.delete(network)); 207 | runningDataDirs.add(network); 208 | }); 209 | 210 | ipcMain.on('goal.status', (event) => { 211 | const window = BrowserWindow.fromWebContents( 212 | event.sender, 213 | )! as ModifiedBrowserWindow; 214 | exec(`"${GOAL}" node status -d "${window.getDataDir()}"`, (err, stdout) => 215 | window.webContents.send('goal.status', err, stdout), 216 | ); 217 | }); 218 | 219 | ipcMain.on('goal.stop', (event) => { 220 | const window = BrowserWindow.fromWebContents( 221 | event.sender, 222 | )! as ModifiedBrowserWindow; 223 | exec(`"${GOAL}" node stop -d "${window.getDataDir()}"`, (err, stdout) => 224 | window.webContents.send('goal.stop', err, stdout), 225 | ); 226 | }); 227 | 228 | ipcMain.on('goal.telemetry', (event, { network, nodeName }) => { 229 | const window = BrowserWindow.fromWebContents( 230 | event.sender, 231 | )! as ModifiedBrowserWindow; 232 | const dataDir = window.getDataDir(); 233 | 234 | let config = JSON.parse( 235 | fs.readFileSync(path.join(dataDir, 'logging.config'), { 236 | encoding: 'utf-8', 237 | }), 238 | ); 239 | 240 | config.Enable = nodeName !== ''; 241 | 242 | if (network === 'algorand.mainnet') { 243 | config.URI = 'https://tel.4160.nodely.io'; 244 | config.Name = nodeName === '' ? '' : `@A1CN:${nodeName}`; 245 | } else { 246 | config.Name = nodeName === '' ? '' : `A1CN:${nodeName}`; 247 | } 248 | 249 | fs.writeFileSync( 250 | path.join(dataDir, 'logging.config'), 251 | JSON.stringify(config), 252 | { 253 | encoding: 'utf-8', 254 | }, 255 | ); 256 | 257 | window.webContents.send( 258 | 'goal.telemetry', 259 | null, 260 | config.Enable ? config.GUID : '', 261 | ); 262 | }); 263 | 264 | ipcMain.on('goal.token', (event) => { 265 | const window = BrowserWindow.fromWebContents( 266 | event.sender, 267 | )! as ModifiedBrowserWindow; 268 | 269 | let err = null; 270 | let stdout = null; 271 | 272 | try { 273 | stdout = fs.readFileSync( 274 | path.join(window.getDataDir(), 'algod.admin.token'), 275 | { 276 | encoding: 'utf-8', 277 | }, 278 | ); 279 | } catch (e) { 280 | err = e; 281 | } 282 | 283 | window.webContents.send('goal.token', err, stdout); 284 | }); 285 | 286 | app.on('will-quit', () => { 287 | // stop all the nodes in runningDataDirs 288 | runningDataDirs.forEach((dataDir) => 289 | exec(`"${GOAL}" node stop -d "${dataDir}"`), 290 | ); 291 | }); 292 | -------------------------------------------------------------------------------- /src/config/algorand.mainnet.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MaxCatchpointDownloadDuration": 43200000000000 3 | } 4 | -------------------------------------------------------------------------------- /src/config/algorand.mainnet.genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "alloc": [ 3 | { 4 | "addr": "737777777777777777777777777777777777777777777777777UFEJ2CI", 5 | "comment": "RewardsPool", 6 | "state": { 7 | "algo": 10000000000000, 8 | "onl": 2 9 | } 10 | }, 11 | { 12 | "addr": "Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA", 13 | "comment": "FeeSink", 14 | "state": { 15 | "algo": 1000000, 16 | "onl": 2 17 | } 18 | }, 19 | { 20 | "addr": "ALGORANDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIN5DNAU", 21 | "comment": "A BIT DOES E NOT BUT E STARTS EVERYTHING LIFE A MANY FORTUNE R BUILD SIMPLER BE THE STARTS PERSEVERES FAVORS A ENOUGH RIPROVANDO POSSIBLE JOURNEY VICTORIA HE BOLD U WITHOUT MEN A K OF BORDERS WHO HE E RACES TOMORROW BUT WHO SINGLE PURPOSE GEOGRAPHICAL PROVANDO A KNOW SUFFOCATES NOT SCIENCE STEP MATHEMATICS OF OR A BRIDGES WALLS TECHNOLOGY TODAY AND WITH AS ET MILES OF THOUSAND VITA SIMPLE TOO MUST AS NOT MADE NOT", 22 | "state": { 23 | "algo": 1000000, 24 | "onl": 2 25 | } 26 | }, 27 | { 28 | "addr": "XQJEJECPWUOXSKMIC5TCSARPVGHQJIIOKHO7WTKEPPLJMKG3D7VWWID66E", 29 | "comment": "AlgorandCommunityAnnouncement", 30 | "state": { 31 | "algo": 10000000, 32 | "onl": 2 33 | } 34 | }, 35 | { 36 | "addr": "VCINCVUX2DBKQ6WP63NOGPEAQAYGHGSGQX7TSH4M5LI5NBPVAGIHJPMIPM", 37 | "comment": "AuctionsMaster", 38 | "state": { 39 | "algo": 1000000000, 40 | "onl": 2 41 | } 42 | }, 43 | { 44 | "addr": "OGP6KK5KCMHT4GOEQXJ4LLNJ7D6P6IH7MV5WZ5EX4ZWACHP75ID5PPEE5E", 45 | "comment": "", 46 | "state": { 47 | "algo": 300000000000000, 48 | "onl": 2 49 | } 50 | }, 51 | { 52 | "addr": "AYBHAG2DAIOG26QEV35HKUBGWPMPOCCQ44MQEY32UOW3EXEMSZEIS37M2U", 53 | "comment": "", 54 | "state": { 55 | "algo": 300000000000000, 56 | "onl": 2 57 | } 58 | }, 59 | { 60 | "addr": "2XKK2L6HOBCYHGIGBS3N365FJKHS733QOX42HIYLSBARUIJHMGQZYAQDRY", 61 | "comment": "", 62 | "state": { 63 | "algo": 300000000000000, 64 | "onl": 2 65 | } 66 | }, 67 | { 68 | "addr": "ZBSPQQG7O5TR5MHPG3D5RS2TIFFD5NMOPR77VUKURMN6HV2BSN224ZHKGU", 69 | "comment": "", 70 | "state": { 71 | "algo": 300000000000000, 72 | "onl": 2 73 | } 74 | }, 75 | { 76 | "addr": "7NQED6NJ4NZU7B5HGGFU2ZEC2UZQYU2SA5S4QOE2EXBVAR4CNAHIXV2XYY", 77 | "comment": "", 78 | "state": { 79 | "algo": 300000000000000, 80 | "onl": 2 81 | } 82 | }, 83 | { 84 | "addr": "RX2ZKVJ43GNYDJNIOB6TIX26U7UEQFUQY46OMHX6CXLMMBHENJIH4YVLUQ", 85 | "comment": "", 86 | "state": { 87 | "algo": 300000000000000, 88 | "onl": 2 89 | } 90 | }, 91 | { 92 | "addr": "RHSKYCCZYYQ2BL6Z63626YUETJMLFGVVV47ED5D55EKIK4YFJ5DQT5CV4A", 93 | "comment": "", 94 | "state": { 95 | "algo": 300000000000000, 96 | "onl": 2 97 | } 98 | }, 99 | { 100 | "addr": "RJS6FDZ46ZZJIONLMMCKDJHYSJNHHAXNABMAVSGH23ULJSEAHZC6AQ6ALE", 101 | "comment": "", 102 | "state": { 103 | "algo": 300000000000000, 104 | "onl": 2 105 | } 106 | }, 107 | { 108 | "addr": "AZ2KKAHF2PJMEEUVN4E2ILMNJCSZLJJYVLBIA7HOY3BQ7AENOVVTXMGN3I", 109 | "comment": "", 110 | "state": { 111 | "algo": 300000000000000, 112 | "onl": 2 113 | } 114 | }, 115 | { 116 | "addr": "CGUKRKXNMEEOK7SJKOGXLRWEZESF24ELG3TAW6LUF43XRT2LX4OVQLU4BQ", 117 | "comment": "", 118 | "state": { 119 | "algo": 300000000000000, 120 | "onl": 2 121 | } 122 | }, 123 | { 124 | "addr": "VVW6BVYHBG7MZQXKAR3OSPUZVAZ66JMQHOBMIBJG6YSPR7SLMNAPA7UWGY", 125 | "comment": "", 126 | "state": { 127 | "algo": 250000000000000, 128 | "onl": 2 129 | } 130 | }, 131 | { 132 | "addr": "N5BGWISAJSYT7MVW2BDTTEHOXFQF4QQH4VKSMKJEOA4PHPYND43D6WWTIU", 133 | "comment": "", 134 | "state": { 135 | "algo": 1740000000000000, 136 | "onl": 2 137 | } 138 | }, 139 | { 140 | "addr": "MKT3JAP2CEI5C4IX73U7QKRUF6JR7KPKE2YD6BLURFVPW6N7CYXVBSJPEQ", 141 | "comment": "", 142 | "state": { 143 | "algo": 158000000000000, 144 | "onl": 2 145 | } 146 | }, 147 | { 148 | "addr": "GVCPSWDNSL54426YL76DZFVIZI5OIDC7WEYSJLBFFEQYPXM7LTGSDGC4SA", 149 | "comment": "", 150 | "state": { 151 | "algo": 49998988000000, 152 | "onl": 1, 153 | "sel": "lZ9z6g0oSlis/8ZlEyOMiGfX0XDUcObfpJEg5KjU0OA=", 154 | "vote": "Kk+5CcpHWIXSMO9GiAvnfe+eNSeRtpDb2telHb6I1EE=", 155 | "voteKD": 10000, 156 | "voteLst": 3000000 157 | } 158 | }, 159 | { 160 | "addr": "M7XKTBQXVQARLS7IVS6NVDHNLJFIAXR2CGGZTUDEKRIHRVLWL5TJFJOL5U", 161 | "comment": "", 162 | "state": { 163 | "algo": 50000000000000, 164 | "onl": 1, 165 | "sel": "Z5gE/m2E/WSuaS5E8aYzO2DugTdSWQdc5W5BroCJdms=", 166 | "vote": "QHHw03LnZQhKvjjIxVj3+qwgohOij2j3TBDMy7V9JMk=", 167 | "voteKD": 10000, 168 | "voteLst": 3000000 169 | } 170 | }, 171 | { 172 | "addr": "QFYWTHPNZBKKZ4XG2OWVNEX6ETBISD2VJZTCMODIZKT3QHQ4TIRJVEDVV4", 173 | "comment": "", 174 | "state": { 175 | "algo": 50000000000000, 176 | "onl": 1, 177 | "sel": "NthIIUyiiRVnU/W13ajFFV4EhTvT5EZR/9N6ZRD/Z7U=", 178 | "vote": "3KtiTLYvHJqa+qkGFj2RcZC77bz9yUYKxBZt8B24Z+c=", 179 | "voteKD": 10000, 180 | "voteLst": 3000000 181 | } 182 | }, 183 | { 184 | "addr": "DPOZQ6HRYLNNWVQL3I4XV4LMK5UZVROKGJBRIYIRNZUBMVHCU4DZWDBHYE", 185 | "comment": "", 186 | "state": { 187 | "algo": 50000000000000, 188 | "onl": 1, 189 | "sel": "PBZ/agWgmwMdmWgt/W0NvdTN/XSTrVhPvRSMjmP5j90=", 190 | "vote": "FDONnMcq1acmIBjJr3vz4kx4Q8ZRZ8oIH8xXRV5c4L8=", 191 | "voteKD": 10000, 192 | "voteLst": 3000000 193 | } 194 | }, 195 | { 196 | "addr": "42GALMKS3HMDB24ZPOR237WQ5QDHL5NIRC3KIA4PCKENJZAD5RP5QPBFO4", 197 | "comment": "", 198 | "state": { 199 | "algo": 50000000000000, 200 | "onl": 1, 201 | "sel": "p7axjoy3Wn/clD7IKoTK2Zahc5ZU+Qkt2POVHKugQU4=", 202 | "vote": "PItHHw+b01XplxRBFmZniqmdm+RyJFYd0fDz+OP4D6o=", 203 | "voteKD": 10000, 204 | "voteLst": 3000000 205 | } 206 | }, 207 | { 208 | "addr": "OXWIMTRZA5TVPABJF534EBBERJG367OLAB6VFN4RAW5P6CQEMXEX7VVDV4", 209 | "comment": "", 210 | "state": { 211 | "algo": 50000000000000, 212 | "onl": 1, 213 | "sel": "RSOWYRM6/LD7MYxlZGvvF+WFGmBZg7UUutdkaWql0Xo=", 214 | "vote": "sYSYFRL7AMJ61egushOYD5ABh9p06C4ZRV/OUSx7o3g=", 215 | "voteKD": 10000, 216 | "voteLst": 3000000 217 | } 218 | }, 219 | { 220 | "addr": "AICDUO6E46YBJRLM4DFJPVRVZGOFTRNPF7UPQXWEPPYRPVGIMQMLY5HLFM", 221 | "comment": "", 222 | "state": { 223 | "algo": 50000000000000, 224 | "onl": 1, 225 | "sel": "0vxjPZqEreAhUt9PHJU2Eerb7gBhMU+PgyEXYLmbifg=", 226 | "vote": "fuc0z/tpiZXBWARCJa4jPdmDvSmun4ShQLFiAxQkOFI=", 227 | "voteKD": 10000, 228 | "voteLst": 3000000 229 | } 230 | }, 231 | { 232 | "addr": "DYATVHCICZA7VVOWZN6OLFFSKUAZ64TZ7WZWCJQBFWL3JL4VBBV6R7Z6IE", 233 | "comment": "", 234 | "state": { 235 | "algo": 50000000000000, 236 | "onl": 1, 237 | "sel": "KO2035CRpp1XmVPOTOF6ICWCw/0I6FgelKxdwPq+gMY=", 238 | "vote": "rlcoayAuud0suR3bvvI0+psi/NzxvAJUFlp+I4ntzkM=", 239 | "voteKD": 10000, 240 | "voteLst": 3000000 241 | } 242 | }, 243 | { 244 | "addr": "6XJH2PJMAXWS4RGE6NBYIS3OZFOPU3LOHYC6MADBFUAALSWNFHMPJUWVSE", 245 | "comment": "", 246 | "state": { 247 | "algo": 50000000000000, 248 | "onl": 1, 249 | "sel": "PgW1dncjs9chAVM89SB0FD4lIrygxrf+uqsAeZw8Qts=", 250 | "vote": "pA4NJqjTAtHGGvZWET9kliq24Go5kEW8w7f1BGAWmKY=", 251 | "voteKD": 10000, 252 | "voteLst": 3000000 253 | } 254 | }, 255 | { 256 | "addr": "EYOZMULFFZZ5QDDMWQ64HKIMUPPNEL3WJMNGAFD43L52ZXTPESBEVJPEZU", 257 | "comment": "", 258 | "state": { 259 | "algo": 50000000000000, 260 | "onl": 1, 261 | "sel": "sfebD2noAbrn1vblMmeCIeGB3BxLGKQDTG4sKSNibFs=", 262 | "vote": "Cuz3REj26J+JhOpf91u6PO6MV5ov5b1K/ii1U1uPD/g=", 263 | "voteKD": 10000, 264 | "voteLst": 3000000 265 | } 266 | }, 267 | { 268 | "addr": "I3345FUQQ2GRBHFZQPLYQQX5HJMMRZMABCHRLWV6RCJYC6OO4MOLEUBEGU", 269 | "comment": "", 270 | "state": { 271 | "algo": 24000000000000, 272 | "onl": 1, 273 | "sel": "MkH9KsdwiFgYtFFWFu48CeejEop1vsyGFG4/kqPIOFg=", 274 | "vote": "RcntidhQqXQIvYjLFtc6HuL335rMnNX92roa2LcC+qQ=", 275 | "voteKD": 10000, 276 | "voteLst": 3000000 277 | } 278 | }, 279 | { 280 | "addr": "6LQH42A4QJ3Y27FGKJWERY3MD65SXM4QQCJJR2HRJYNB427IQ73YBI3YFY", 281 | "comment": "", 282 | "state": { 283 | "algo": 24000000000000, 284 | "onl": 1, 285 | "sel": "nF3mu9Bu0Ad5MIrT31NgTxxrsZOXc4u1+WCvaPQTYEQ=", 286 | "vote": "NaqWR/7FzOq/MiHb3adO6+J+kvnQKat8NSqEmoEkVfE=", 287 | "voteKD": 10000, 288 | "voteLst": 3000000 289 | } 290 | }, 291 | { 292 | "addr": "3V2MC7WJGAFU2EHWBHEETIMJVFJNAT4KKWVPOMJFJIM6ZPWEJRJ4POTXGI", 293 | "comment": "", 294 | "state": { 295 | "algo": 24000000000000, 296 | "onl": 1, 297 | "sel": "3i4K8zdmnf1kxwgcNmI3x50iIwAxDmLMvoQEhjzhado=", 298 | "vote": "wfJWa0kby76rqX2yvCD/aCfJdNt+qItylDPQiuAWFkQ=", 299 | "voteKD": 10000, 300 | "voteLst": 3000000 301 | } 302 | }, 303 | { 304 | "addr": "FTXSKED23VEXNW442T2JKNPPNUC2WKFNRWBVQTFMT7HYX365IVLZXYILAI", 305 | "comment": "", 306 | "state": { 307 | "algo": 24000000000000, 308 | "onl": 1, 309 | "sel": "icuL7ehcGonAcJ02Zy4MIHqcT+Sp1R1UURNCYJQHmo4=", 310 | "vote": "tmFcj3v7X5DDxKI1IDbGdhXh3a5f0Ab1ftltM7TgIDE=", 311 | "voteKD": 10000, 312 | "voteLst": 3000000 313 | } 314 | }, 315 | { 316 | "addr": "IAOW7PXLCDGLKMIQF26IXFF4THSQMU662MUU6W5KPOXHIVKHYFLYRWOUT4", 317 | "comment": "", 318 | "state": { 319 | "algo": 24000000000000, 320 | "onl": 1, 321 | "sel": "zTn9rl/8Y2gokMdFyFP/pKg4eP02arkxlrBZIS94vPI=", 322 | "vote": "a0pX68GgY7u8bd2Z3311+Mtc6yDnESZmi9k8zJ0oHzY=", 323 | "voteKD": 10000, 324 | "voteLst": 3000000 325 | } 326 | }, 327 | { 328 | "addr": "4NRNE5RIGC2UGOMGMDR6L5YMQUV3Q76TPOR7TDU3WEMJLMC6BSBEKPJ2SY", 329 | "comment": "", 330 | "state": { 331 | "algo": 24000000000000, 332 | "onl": 1, 333 | "sel": "orSV2VHPY8m5ckEHGwK0r+SM9jq4BujAICXegAUAecI=", 334 | "vote": "NJ9tisH+7+S29m/uMymFTD8X02/PKU0JUX1ghnLCzkw=", 335 | "voteKD": 10000, 336 | "voteLst": 3000000 337 | } 338 | }, 339 | { 340 | "addr": "E2EIMPLDISONNZLXONGMC33VBYOIBC2R7LVOS4SYIEZYJQK6PYSAPQL7LQ", 341 | "comment": "", 342 | "state": { 343 | "algo": 24000000000000, 344 | "onl": 1, 345 | "sel": "XM2iW9wg9G5TyOfVu9kTS80LDIqcEPkJsgxaZll3SWA=", 346 | "vote": "p/opFfDOsIomj5j7pAYU+G/CNUIwvD2XdEer6dhGquQ=", 347 | "voteKD": 10000, 348 | "voteLst": 3000000 349 | } 350 | }, 351 | { 352 | "addr": "APDO5T76FB57LNURPHTLAGLQOHUQZXYHH2ZKR4DPQRKK76FB4IAOBVBXHQ", 353 | "comment": "", 354 | "state": { 355 | "algo": 24000000000000, 356 | "onl": 1, 357 | "sel": "5k2vclbUQBE6zBl45F3kGSv1PYhE2k9wZjxyxoPlnwA=", 358 | "vote": "3dcLRSckm3wd9KB0FBRxub3meIgT6lMZnv5F08GJgEo=", 359 | "voteKD": 10000, 360 | "voteLst": 3000000 361 | } 362 | }, 363 | { 364 | "addr": "3KJTYHNHK37G2JDZJPV55IHBADU22TX2FPJZJH43MY64IFWKVNMP2F4JZE", 365 | "comment": "", 366 | "state": { 367 | "algo": 24000000000000, 368 | "onl": 1, 369 | "sel": "o5e9VLqMdmJas5wRovfYFHgQ+Z6sQoATf3a6j0HeIXU=", 370 | "vote": "rG7J8pPAW+Xtu5pqMIJOG9Hxdlyewtf9zPHEKR2Q6OE=", 371 | "voteKD": 10000, 372 | "voteLst": 3000000 373 | } 374 | }, 375 | { 376 | "addr": "IVKDCE6MS44YVGMQQFVXCDABW2HKULKIXMLDS2AEOIA6P2OGMVHVJ64MZI", 377 | "comment": "", 378 | "state": { 379 | "algo": 24000000000000, 380 | "onl": 1, 381 | "sel": "XgUrwumD7oin/rG3NKwywBSsTETg/aWg9MjCDG61Ybg=", 382 | "vote": "sBPEGGrEqcQMdT+iq2ududNxCa/1HcluvsosO1SkE/k=", 383 | "voteKD": 10000, 384 | "voteLst": 3000000 385 | } 386 | }, 387 | { 388 | "addr": "2WDM5XFF7ONWFANPE5PBMPJLVWOEN2BBRLSKJ37PQYW5WWIHEFT3FV6N5Y", 389 | "comment": "", 390 | "state": { 391 | "algo": 24000000000000, 392 | "onl": 1, 393 | "sel": "Lze5dARJdb1+Gg6ui8ySIi+LAOM3P9dKiHKB9HpMM6A=", 394 | "vote": "ys4FsqUNQiv+N0RFtr0Hh9OnzVcxXS6cRVD/XrLgW84=", 395 | "voteKD": 10000, 396 | "voteLst": 3000000 397 | } 398 | }, 399 | { 400 | "addr": "EOZWAIPQEI23ATBWQ5J57FUMRMXADS764XLMBTSOLVKPMK5MK5DBIS3PCY", 401 | "comment": "", 402 | "state": { 403 | "algo": 24000000000000, 404 | "onl": 1, 405 | "sel": "jtmLcJhaAknJtA1cS5JPZil4SQ5SKh8P0w1fUw3X0CE=", 406 | "vote": "pyEtTxJAas/j+zi/N13b/3LB4UoCar1gfcTESl0SI2I=", 407 | "voteKD": 10000, 408 | "voteLst": 3000000 409 | } 410 | }, 411 | { 412 | "addr": "REMF542E5ZFKS7SGSNHTYB255AUITEKHLAATWVPK3CY7TAFPT6GNNCHH6M", 413 | "comment": "", 414 | "state": { 415 | "algo": 24000000000000, 416 | "onl": 1, 417 | "sel": "8ggWPvRpSkyrjxoh1SVS9PiSjff2azWtH0HFadwI9Ck=", 418 | "vote": "Ej/dSkWbzRf09RAuWZfC4luRPNuqkLFCSGYXDcOtwic=", 419 | "voteKD": 10000, 420 | "voteLst": 3000000 421 | } 422 | }, 423 | { 424 | "addr": "T4UBSEAKK7JHT7RNLXVHDRW72KKFJITITR54J464CAGE5FGAZFI3SQH3TI", 425 | "comment": "", 426 | "state": { 427 | "algo": 24000000000000, 428 | "onl": 1, 429 | "sel": "eIB8MKaG2lyJyM9spk+b/Ap/bkbo9bHfvF9f8T51OQk=", 430 | "vote": "7xuBsE5mJaaRAdm5wnINVwm4SgPqKwJTAS1QBQV3sEc=", 431 | "voteKD": 10000, 432 | "voteLst": 3000000 433 | } 434 | }, 435 | { 436 | "addr": "YUDNQMOHAXC4B3BAMRMMQNFDFZ7GYO2HUTBIMNIP7YQ4BL57HZ5VM3AFYU", 437 | "comment": "", 438 | "state": { 439 | "algo": 24000000000000, 440 | "onl": 1, 441 | "sel": "CSTCDvvtsJB0VYUcl3oRXyiJfhm3CtqvRIuFYZ69Z68=", 442 | "vote": "uBK1TH4xKdWfv5nnnHkvYssI0tyhWRFZRLHgVt9TE1k=", 443 | "voteKD": 10000, 444 | "voteLst": 3000000 445 | } 446 | }, 447 | { 448 | "addr": "4SZTEUQIURTRT37FCI3TRMHSYT5IKLUPXUI7GWC5DZFXN2DGTATFJY5ABY", 449 | "comment": "", 450 | "state": { 451 | "algo": 24000000000000, 452 | "onl": 1, 453 | "sel": "THGOlrqElX13xMqeLUPy6kooTbXjiyrUoZfVccnHrfI=", 454 | "vote": "k4hde2Q3Zl++sQobo01U8heZd/X0GIX1nyqM8aI/hCY=", 455 | "voteKD": 10000, 456 | "voteLst": 3000000 457 | } 458 | }, 459 | { 460 | "addr": "UEDD34QFEMWRGYCBLKZIEHPKSTNBFSRMFBHRJPY3O2JPGKHQCXH4IY6XRI", 461 | "comment": "", 462 | "state": { 463 | "algo": 24000000000000, 464 | "onl": 1, 465 | "sel": "jE+AUFvtp2NJsfNeUZeXdWt0X6I58YOgY+z/HB17GDs=", 466 | "vote": "lmnYTjg1FhRNAR9TwVmOahVr5Z+7H1GO6McmvOZZRTQ=", 467 | "voteKD": 10000, 468 | "voteLst": 3000000 469 | } 470 | }, 471 | { 472 | "addr": "HHZQOGQKMQDLBEL3HXMDX7AGTNORYVZ4JFDWVSL5QLWMD3EXOIAHDI5L7M", 473 | "comment": "", 474 | "state": { 475 | "algo": 24000000000000, 476 | "onl": 1, 477 | "sel": "Hajdvzem2rR2GjLmCG+98clHZFY5Etlp0n+x/gQTGj0=", 478 | "vote": "2+Ie4MDWC6o/SfFSqev1A7UAkzvKRESI42b4NKS6Iw8=", 479 | "voteKD": 10000, 480 | "voteLst": 3000000 481 | } 482 | }, 483 | { 484 | "addr": "XRTBXPKH3DXDJ5OLQSYXOGX3DJ3U5NR6Y3LIVIWMK7TY33YW4I2NJZOTVE", 485 | "comment": "", 486 | "state": { 487 | "algo": 24000000000000, 488 | "onl": 1, 489 | "sel": "5qe7rVoQfGdIUuDbhP2ABWivCoCstKbUsjdmYY76akA=", 490 | "vote": "3J3O9DyJMWKvACubUK9QvmCiArtZR7yFHWG7k7+apdQ=", 491 | "voteKD": 10000, 492 | "voteLst": 3000000 493 | } 494 | }, 495 | { 496 | "addr": "JJFGCPCZPYRLOUYBZVC4F7GRPZ5CLB6BMTVRGNDP7GRGXL6GG4JEN7DL54", 497 | "comment": "", 498 | "state": { 499 | "algo": 24000000000000, 500 | "onl": 1, 501 | "sel": "YoRFAcTiOgJcLudNScYstbaKJ8anrrHwQMZAffWMqYE=", 502 | "vote": "VQFKlDdxRqqqPUQ/mVoF8xZS9BGxUtTnPUjYyKnOVRA=", 503 | "voteKD": 10000, 504 | "voteLst": 3000000 505 | } 506 | }, 507 | { 508 | "addr": "4VNSA2GZVUD5ZNO62OVVNP4NEL2LIEE5N3MZEK4BKH62KGKRLVINFZYTZM", 509 | "comment": "", 510 | "state": { 511 | "algo": 100000000000000, 512 | "onl": 2 513 | } 514 | }, 515 | { 516 | "addr": "IVCEEIH2Q32DZNRTS5XFVEFFAQGERNZHHQT6S4UPY7ORJMHIQDSTX7YM4E", 517 | "comment": "", 518 | "state": { 519 | "algo": 408400000000000, 520 | "onl": 2 521 | } 522 | }, 523 | { 524 | "addr": "PLFHBIRGM3ZWGAMCXTREX2N537TWOMFIQXHFO2ZGQOEPZU473SYBVGVA5M", 525 | "comment": "", 526 | "state": { 527 | "algo": 1011600000000000, 528 | "onl": 2 529 | } 530 | }, 531 | { 532 | "addr": "KF7X4ZABZUQU7IFMHSKLDKWCS4F3GZLOLJRDAK5KMEMDAGU32CX36CJQ5M", 533 | "comment": "", 534 | "state": { 535 | "algo": 10000000000000, 536 | "onl": 2 537 | } 538 | }, 539 | { 540 | "addr": "BTEESEYQMFLWZKULSKLNDELYJTOOQK6ZT4FBCW3TOZQ55NZYLOO6BRQ5K4", 541 | "comment": "", 542 | "state": { 543 | "algo": 36199095000000, 544 | "onl": 2 545 | } 546 | }, 547 | { 548 | "addr": "E36JOZVSZZDXKSERASLAWQE4NU67HC7Q6YDOCG7P7IRRWCPSWXOI245DPA", 549 | "comment": "", 550 | "state": { 551 | "algo": 20000000000000, 552 | "onl": 2 553 | } 554 | }, 555 | { 556 | "addr": "I5Q6RRN44OZWYMX6YLWHBGEVPL7S3GBUCMHZCOOLJ245TONH7PERHJXE4A", 557 | "comment": "", 558 | "state": { 559 | "algo": 20000000000000, 560 | "onl": 2 561 | } 562 | }, 563 | { 564 | "addr": "2GYS272T3W2AP4N2VX5BFBASVNLWN44CNVZVKLWMMVPZPHVJ52SJPPFQ2I", 565 | "comment": "", 566 | "state": { 567 | "algo": 40000000000000, 568 | "onl": 2 569 | } 570 | }, 571 | { 572 | "addr": "D5LSV2UGT4JJNSLJ5XNIF52WP4IHRZN46ZGWH6F4QEF4L2FLDYS6I6R35Y", 573 | "comment": "", 574 | "state": { 575 | "algo": 20000000000000, 576 | "onl": 2 577 | } 578 | }, 579 | { 580 | "addr": "UWMSBIP2CGCGR3GYVUIOW3YOMWEN5A2WRTTBH6Y23KE3MOVFRHNXBP6IOE", 581 | "comment": "", 582 | "state": { 583 | "algo": 20000000000000, 584 | "onl": 2 585 | } 586 | }, 587 | { 588 | "addr": "OF3MKZZ3L5ZN7AZ46K7AXJUI4UWJI3WBRRVNTDKYVZUHZAOBXPVR3DHINE", 589 | "comment": "", 590 | "state": { 591 | "algo": 40000000000000, 592 | "onl": 2 593 | } 594 | }, 595 | { 596 | "addr": "2PPWE36YUMWUVIFTV2A6U4MLZLGROW4GHYIRVHMUCHDH6HCNVPUKPQ53NY", 597 | "comment": "", 598 | "state": { 599 | "algo": 440343426000000, 600 | "onl": 2 601 | } 602 | }, 603 | { 604 | "addr": "JRGRGRW4HYBNAAHR7KQLLBAGRSPOYY6TRSINKYB3LI5S4AN247TANH5IQY", 605 | "comment": "", 606 | "state": { 607 | "algo": 362684706000000, 608 | "onl": 2 609 | } 610 | }, 611 | { 612 | "addr": "D7YVVQJXJEFOZYUHJLIJBW3ATCAW46ML62VYRJ3SMGLOHMWYH4OS3KNHTU", 613 | "comment": "", 614 | "state": { 615 | "algo": 10000000000000, 616 | "onl": 2 617 | } 618 | }, 619 | { 620 | "addr": "PZJKH2ILW2YDZNUIYQVJZ2MANRSMK6LCHAFSAPYT6R3L3ZCWKYRDZXRVY4", 621 | "comment": "", 622 | "state": { 623 | "algo": 10000000000000, 624 | "onl": 2 625 | } 626 | }, 627 | { 628 | "addr": "3MODEFJVPGUZH3HDIQ6L2MO3WLJV3FK3XSWKFBHUGZDCHXQMUKD4B7XLMI", 629 | "comment": "", 630 | "state": { 631 | "algo": 130000000000000, 632 | "onl": 2 633 | } 634 | }, 635 | { 636 | "addr": "WNSA5P6C5IIH2UJPQWJX6FRNPHXY7XZZHOWLSW5ZWHOEHBUW4AD2H6TZGM", 637 | "comment": "", 638 | "state": { 639 | "algo": 130000000000000, 640 | "onl": 2 641 | } 642 | }, 643 | { 644 | "addr": "OO65J5AIFDS6255WL3AESTUGJD5SUV47RTUDOUGYHEIME327GX7K2BGC6U", 645 | "comment": "", 646 | "state": { 647 | "algo": 40000000000000, 648 | "onl": 2 649 | } 650 | }, 651 | { 652 | "addr": "DM6A24ZWHRZRM2HWXUHAUDSAACO7VKEZAOC2THWDXH4DX5L7LSO3VF2OPU", 653 | "comment": "", 654 | "state": { 655 | "algo": 20000000000000, 656 | "onl": 2 657 | } 658 | }, 659 | { 660 | "addr": "NTJJSFM75RADUOUGOBHZB7IJGO7NLVBWA66EYOOPU67H7LYIXVSPSI7BTA", 661 | "comment": "", 662 | "state": { 663 | "algo": 18099548000000, 664 | "onl": 2 665 | } 666 | }, 667 | { 668 | "addr": "DAV2AWBBW4HBGIL2Z6AAAWDWRJPTOQD6BSKU2CFXZQCOBFEVFEJ632I2LY", 669 | "comment": "", 670 | "state": { 671 | "algo": 1000000000000, 672 | "onl": 2 673 | } 674 | }, 675 | { 676 | "addr": "M5VIY6QPSMALVVPVG5LVH35NBMH6XJMXNWKWTARGGTEEQNQ3BHPQGYP5XU", 677 | "comment": "", 678 | "state": { 679 | "algo": 20000000000000, 680 | "onl": 2 681 | } 682 | }, 683 | { 684 | "addr": "WZZLVKMCXJG3ICVZSVOVAGCCN755VHJKZWVSVQ6JPSRQ2H2OSPOOZKW6DQ", 685 | "comment": "", 686 | "state": { 687 | "algo": 45248869000000, 688 | "onl": 2 689 | } 690 | }, 691 | { 692 | "addr": "XEJLJUZRQOLBHHSOJJUE4IWI3EZOM44P646UDKHS4AV2JW7ZWBWNFGY6BU", 693 | "comment": "", 694 | "state": { 695 | "algo": 20000000000000, 696 | "onl": 2 697 | } 698 | }, 699 | { 700 | "addr": "OGIPDCRJJPNVZ6X6NBQHMTEVKJVF74QHZIXVLABMGUKZWNMEH7MNXZIJ7Q", 701 | "comment": "", 702 | "state": { 703 | "algo": 40000000000000, 704 | "onl": 2 705 | } 706 | }, 707 | { 708 | "addr": "G47R73USFN6FJJQTI3JMYQXO7F6H4LRPBCTTAD5EZWPWY2WCG64AVPCYG4", 709 | "comment": "", 710 | "state": { 711 | "algo": 10000000000000, 712 | "onl": 2 713 | } 714 | }, 715 | { 716 | "addr": "PQ5T65QB564NMIY6HXNYZXTFRSTESUEFIF2C26ZZKIZE6Q4R4XFP5UYYWI", 717 | "comment": "", 718 | "state": { 719 | "algo": 5000000000000, 720 | "onl": 2 721 | } 722 | }, 723 | { 724 | "addr": "R6S7TRMZCHNQPKP2PGEEJ6WYUKMTURNMM527ZQXABTHFT5GBVMF6AZAL54", 725 | "comment": "", 726 | "state": { 727 | "algo": 1000000000000, 728 | "onl": 2 729 | } 730 | }, 731 | { 732 | "addr": "36LZKCBDUR5EHJ74Q6UWWNADLVJOHGCPBBQ5UTUM3ILRTQLA6RYYU4PUWQ", 733 | "comment": "", 734 | "state": { 735 | "algo": 5000000000000, 736 | "onl": 2 737 | } 738 | }, 739 | { 740 | "addr": "JRHPFMSJLU42V75NTGFRQIALCK6RHTEK26QKLWCH2AEEAFNAVEXWDTA5AM", 741 | "comment": "", 742 | "state": { 743 | "algo": 40000000000000, 744 | "onl": 2 745 | } 746 | }, 747 | { 748 | "addr": "64VZVS2LFZXWA5W3S657W36LWGP34B7XLMDIF4ROXBTPADD7SR5WNUUYJE", 749 | "comment": "", 750 | "state": { 751 | "algo": 171945701000000, 752 | "onl": 2 753 | } 754 | }, 755 | { 756 | "addr": "TXDBSEZPFP2UB6BDNFCHCZBTPONIIQVZGABM4UBRHVAAPR5NE24QBL6H2A", 757 | "comment": "", 758 | "state": { 759 | "algo": 60000000000000, 760 | "onl": 2 761 | } 762 | }, 763 | { 764 | "addr": "XI5TYT4XPWUHE4AMDDZCCU6M4AP4CAI4VTCMXXUNS46I36O7IYBQ7SL3D4", 765 | "comment": "", 766 | "state": { 767 | "algo": 40000000000000, 768 | "onl": 2 769 | } 770 | }, 771 | { 772 | "addr": "Y6ZPKPXF2QHF6ULYQXVHM7NPI3L76SP6QHJHK7XTNPHNXDEUTJPRKUZBNE", 773 | "comment": "", 774 | "state": { 775 | "algo": 40000000000000, 776 | "onl": 2 777 | } 778 | }, 779 | { 780 | "addr": "6LY2PGUJLCK4Q75JU4IX5VWVJVU22VGJBWPZOFP3752UEBIUBQRNGJWIEA", 781 | "comment": "", 782 | "state": { 783 | "algo": 40000000000000, 784 | "onl": 2 785 | } 786 | }, 787 | { 788 | "addr": "L7AGFNAFJ6Z2FYCX3LXE4ZSERM2VOJF4KPF7OUCMGK6GWFXXDNHZJBEC2E", 789 | "comment": "", 790 | "state": { 791 | "algo": 10000000000000, 792 | "onl": 2 793 | } 794 | }, 795 | { 796 | "addr": "RYXX5U2HMWGTPBG2UDLDT6OXDDRCK2YGL7LFAKYNBLRGZGYEJLRMGYLSVU", 797 | "comment": "", 798 | "state": { 799 | "algo": 40000000000000, 800 | "onl": 2 801 | } 802 | }, 803 | { 804 | "addr": "S263NYHFQWZYLINTBELLMIRMAJX6J5CUMHTECTGGVZUKUN2XY6ND2QBZVY", 805 | "comment": "", 806 | "state": { 807 | "algo": 21647524000000, 808 | "onl": 2 809 | } 810 | }, 811 | { 812 | "addr": "AERTZIYYGK3Q364M6DXPKSRRNSQITWYEDGAHXC6QXFCF4GPSCCSISAGCBY", 813 | "comment": "", 814 | "state": { 815 | "algo": 19306244000000, 816 | "onl": 2 817 | } 818 | }, 819 | { 820 | "addr": "34UYPXOJA6WRTWRNH5722LFDLWT23OM2ZZTCFQ62EHQI6MM3AJIAKOWDVQ", 821 | "comment": "", 822 | "state": { 823 | "algo": 10000000000000, 824 | "onl": 2 825 | } 826 | }, 827 | { 828 | "addr": "EDVGNQL6APUFTIGFZHASIEWGJRZNWGIKJE64B72V36IQM2SJPOAG2MJNQE", 829 | "comment": "", 830 | "state": { 831 | "algo": 20000000000000, 832 | "onl": 2 833 | } 834 | }, 835 | { 836 | "addr": "RKKLUIIGR75DFWGQOMJB5ZESPT7URDPC7QHGYKM4MAJ4OEL2J5WAQF6Z2Q", 837 | "comment": "", 838 | "state": { 839 | "algo": 40000000000000, 840 | "onl": 2 841 | } 842 | }, 843 | { 844 | "addr": "M4TNVJLDZZFAOH2M24BE7IU72KUX3P6M2D4JN4WZXW7WXH3C5QSHULJOU4", 845 | "comment": "", 846 | "state": { 847 | "algo": 10000000000000, 848 | "onl": 2 849 | } 850 | }, 851 | { 852 | "addr": "WQL6MQS5SPK3CR3XUPYMGOUSCUC5PNW5YQPLGEXGKVRK3KFKSAZ6JK4HXQ", 853 | "comment": "", 854 | "state": { 855 | "algo": 10000000000000, 856 | "onl": 2 857 | } 858 | }, 859 | { 860 | "addr": "36JTK4PKUBJGVCWKXZTAG6VLJRXWZXQVPQQSYODSN6WEGVHOWSVK6O54YU", 861 | "comment": "", 862 | "state": { 863 | "algo": 10000000000000, 864 | "onl": 2 865 | } 866 | }, 867 | { 868 | "addr": "YFOAYI4SNXJR2DBEZ3O6FJOFSEQHWD7TYROCNDWF6VLBGLNJMRRHDXXZUI", 869 | "comment": "", 870 | "state": { 871 | "algo": 30000000000000, 872 | "onl": 2 873 | } 874 | }, 875 | { 876 | "addr": "XASOPHD3KK3NNI5IF2I7S7U55RGF22SG6OEICVRMCTMMGHT3IBOJG7QWBU", 877 | "comment": "", 878 | "state": { 879 | "algo": 40000000000000, 880 | "onl": 2 881 | } 882 | }, 883 | { 884 | "addr": "H2AUGBLVQFHHFLFEPJ6GGJ7PBQITEN2GE6T7JZCALBKNU7Q52AVJM5HOYU", 885 | "comment": "", 886 | "state": { 887 | "algo": 10000000000000, 888 | "onl": 2 889 | } 890 | }, 891 | { 892 | "addr": "GX3XLHSRMFTADVKJBBQBTZ6BKINW6ZO5JHXWGCWB4CPDNPDQ2PIYN4AVHQ", 893 | "comment": "", 894 | "state": { 895 | "algo": 40000000000000, 896 | "onl": 2 897 | } 898 | }, 899 | { 900 | "addr": "VBJBJ4VC3IHUTLVLWMBON36Y5MPAMPV4DNGW5FQ47GRLPT7JR5PQOUST2E", 901 | "comment": "", 902 | "state": { 903 | "algo": 4524887000000, 904 | "onl": 2 905 | } 906 | }, 907 | { 908 | "addr": "7AQVTOMB5DJRSUM4LPLVF6PY3Y5EBDF4RZNDIWNW4Z63JYTAQCPQ62IZFE", 909 | "comment": "", 910 | "state": { 911 | "algo": 50000000000000, 912 | "onl": 2 913 | } 914 | }, 915 | { 916 | "addr": "B4ZIHKD4VYLA4BAFEP7KUHZD7PNWXW4QLCHCNKWRENJ2LYVEOIYA3ZX6IA", 917 | "comment": "", 918 | "state": { 919 | "algo": 40000000000000, 920 | "onl": 2 921 | } 922 | }, 923 | { 924 | "addr": "G5RGT3EENES7UVIQUHXMJ5APMOGSW6W6RBC534JC6U2TZA4JWC7U27RADE", 925 | "comment": "", 926 | "state": { 927 | "algo": 10000000000000, 928 | "onl": 2 929 | } 930 | }, 931 | { 932 | "addr": "5AHJFDLAXVINK34IGSI3JA5OVRVMPCWLFEZ6TA4I7XUZ7I6M34Q56DUYIM", 933 | "comment": "", 934 | "state": { 935 | "algo": 20000000000000, 936 | "onl": 2 937 | } 938 | } 939 | ], 940 | "fees": "Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA", 941 | "id": "v1.0", 942 | "network": "mainnet", 943 | "proto": "https://github.com/algorandfoundation/specs/tree/5615adc36bad610c7f165fa2967f4ecfa75125f0", 944 | "rwd": "737777777777777777777777777777777777777777777777777UFEJ2CI", 945 | "timestamp": 1560211200 946 | } -------------------------------------------------------------------------------- /src/config/voi.mainnet.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DNSBootstrapID": ".mainnet-voi.network?backup=.mainnet-voi.net&dedup=.mainnet-voi.(network|net)" 3 | } 4 | -------------------------------------------------------------------------------- /src/config/voi.mainnet.genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "alloc": [ 3 | { 4 | "addr": "UVPIYH5R7F7ZOLRPE2SLGISO4C5Q72DWJGKARZOXF4HIL67IDCFLEWLMUA", 5 | "comment": "RewardsPool", 6 | "state": { 7 | "algo": 10000000000, 8 | "onl": 2 9 | } 10 | }, 11 | { 12 | "addr": "TBEIGCNK4UCN3YDP2NODK3MJHTUZMYS3TABRM2MVSI2MPUR2V36E5JYHSY", 13 | "comment": "FeeSink", 14 | "state": { 15 | "algo": 10000000000, 16 | "onl": 2 17 | } 18 | }, 19 | { 20 | "addr": "6YOWT3H6AAPFAW4PNZNRXCSLQKFGCYDDKTPU6KWTVICNN44QXPLQSRUHFY", 21 | "comment": "", 22 | "state": { 23 | "algo": 10000000000, 24 | "onl": 1, 25 | "sel": "7Uc3MbKBYZzXRYDFKU+z4sl0MdeR3E/3YbmzkjZ01oA=", 26 | "stprf": "NIsERgq7CzWrcgMMznZ0up7mj7nHmiqtq0oUHoUzw3+ZL38/9Cu2mpDAiQsbRw0E/zZOh8pGE/OeT8Ii/4G4dQ==", 27 | "vote": "0fF4jWPJjQXZPxo81S4NnJkKFNRciGTEdP86kd9toWQ=", 28 | "voteKD": 3163, 29 | "voteLst": 10000000 30 | } 31 | }, 32 | { 33 | "addr": "GFX47SJSYT72LMXPIHRWIWRVUAQNC3GBSLNKX445MIYVULCJDOYEJUMP24", 34 | "comment": "", 35 | "state": { 36 | "algo": 10000000000, 37 | "onl": 1, 38 | "sel": "orkoWa0lHNkrI5ID9+V8f5m2p/TogVyCHecFfLZprTo=", 39 | "stprf": "2DPAqFo7i0t7WG2UnIQHx1joYuGmB2eFAWcWVDBMVEra96gEM8DY3jPunCvy1qQp+3YGqLb766rbFAh+Z15rqg==", 40 | "vote": "lw6mmZi5k6V/IS9KMuFOEGiwp7EpH1kB9ypO5eF6clI=", 41 | "voteKD": 3163, 42 | "voteLst": 10000000 43 | } 44 | }, 45 | { 46 | "addr": "J5GTWOPA6DSS2RCHJG3TNE64JXVNOG2EG53636VRT7ZXFK2TDW4ZHMATWM", 47 | "comment": "", 48 | "state": { 49 | "algo": 10000000000, 50 | "onl": 1, 51 | "sel": "8F+p88Gk792ozYg9yT3/lEQuJBtQWQzhBY+tuPYTyU8=", 52 | "stprf": "dQVx8lfq7PKGifd1xUbScQXDEdSA+tT7yoYeTjCacHz93PPwfxKm1IjBllH+K1qYsIf+j0tbiMubk1tTPPI3vA==", 53 | "vote": "gPvaW7YT9wjX6x33JGlPtyh+mPFpwj7Q0WzRFOPzXhg=", 54 | "voteKD": 3163, 55 | "voteLst": 10000000 56 | } 57 | }, 58 | { 59 | "addr": "VCUJH5EQJQWMCXGOGYAZIHMJXTQXJRPVCVKQRPMM2SG3QWQ3QIOIGODH3M", 60 | "comment": "", 61 | "state": { 62 | "algo": 10000000000, 63 | "onl": 1, 64 | "sel": "q7s8K1UvMryHj8p1t6YEdFJe+a99QZfqm1P22xRmiNQ=", 65 | "stprf": "ymTdy+CVutXHcMPNmJz2yZcR+1UkBpas5E/dbhJeb/rDXCIfqqdCxwYkcH+nBe+0Noa+piBeo5qVGX7GW6AflQ==", 66 | "vote": "XqQ0Td4vpqXRMyU8O9xcwacR5dqn0lVll4c8cjVdx9M=", 67 | "voteKD": 3163, 68 | "voteLst": 10000000 69 | } 70 | }, 71 | { 72 | "addr": "4HCISKZ2QAVJHPRUGCF5FLCOCPFDKUHB7UVIFVWCUCL226BSJ3CGGQCJW4", 73 | "comment": "", 74 | "state": { 75 | "algo": 10000000000, 76 | "onl": 1, 77 | "sel": "zXnystGkbKTV2LDc+HEtSxMVWR4Mv+toK7o1DOk0lnU=", 78 | "stprf": "IZozu6uKdR8lOkO37OlJ8nxQEakNqn/XG5uszgGWqzmNKCI+zt463otZnQcg/qpKMgW9KDCMi9FIQAuJ5A/DNg==", 79 | "vote": "E+hwAnw1AH5VrrU46JbScqp+rzQD99CZmtESgfgtQ3o=", 80 | "voteKD": 3163, 81 | "voteLst": 10000000 82 | } 83 | }, 84 | { 85 | "addr": "5AEQM2JQ2IMVPRQ7DOWOIFU3WYHF6FJUQLH266YR3NHYFDWIOJH646MBQU", 86 | "comment": "", 87 | "state": { 88 | "algo": 10000000000, 89 | "onl": 1, 90 | "sel": "soeEeXCf0bXeyf1I3K9GEoJFqxuKsJNiVSFZCoOkZWA=", 91 | "stprf": "2saACyEsq3MZ/wUVkpUR4DtuBrdnG3THaujvDfGDr7LAfVqhdsHCnZizv97UuF+p6FBzPNiK0okvIfHIKT1itw==", 92 | "vote": "7FdDSxw2E9/EGvsuYVjXfnbSVbc0my79u3pei4TWk9o=", 93 | "voteKD": 3163, 94 | "voteLst": 10000000 95 | } 96 | }, 97 | { 98 | "addr": "OMJ34AIYPSC3YCTASOMS32Q5RR4NUZMK726MBE2QBNUVUCY3JO3YO7MGZE", 99 | "comment": "", 100 | "state": { 101 | "algo": 10000000000, 102 | "onl": 1, 103 | "sel": "FlU/Bynl6rKExKpivvoh7V022RJxBIx4PWlmGobRN2I=", 104 | "stprf": "6Uyh+K2s1XnvVlH5QgQGXGtgeQjM6n9Kjd+s3QFLJvpYoxQOohrLnOiPOZnsg3HciSOYLH/ucE6Avthq85Bzsw==", 105 | "vote": "GiCilt1LBKZZmDeG5fw1e74lbw6Cc+Kj2hKS2mB/ZrM=", 106 | "voteKD": 3163, 107 | "voteLst": 10000000 108 | } 109 | }, 110 | { 111 | "addr": "R3V36DX66X2QWZST3HN53G3WUJW67JNTHAHMAYDEQQ4PKVPCHHDYXJX3M4", 112 | "comment": "", 113 | "state": { 114 | "algo": 10000000000, 115 | "onl": 1, 116 | "sel": "Bzulp4vPU4qX2fsH1lI4nEKjWUnlb/L0KJ76X1XW8p8=", 117 | "stprf": "1yIIsY/2JWPBamn9jskx5Bj9LP+g/9Zb8LO1rwbnaAjI7s8y5qQI6DvkX9NAWLvvwjUpKL+PwCDRLv+T5TLwcQ==", 118 | "vote": "z6G4AFRqIqgqknSq7scVNx7gyS7UTPCMK/WlsTJsAMg=", 119 | "voteKD": 3163, 120 | "voteLst": 10000000 121 | } 122 | }, 123 | { 124 | "addr": "PTURD4BBWXB4ZDM624BG3YZHUOKCJIDKRKV7TQIVBUIZZUFBVJRW3ZDZ5Y", 125 | "comment": "", 126 | "state": { 127 | "algo": 10000000000, 128 | "onl": 1, 129 | "sel": "Gkljfe6CuaSiIkmsX1QXEzCViOk59ta0r8ukWKB4sbU=", 130 | "stprf": "T7OHtO2/pE0RLzK9Yrent1viUq/lCojnQPV23TM0uC27Qj5lYHf9B3CCC+bpX/E8fGgihbAlntYST1xvGyov4g==", 131 | "vote": "ppzIFpozaHU3mSaJdKj/BtIAVPh56+eXG5hIiIbYM+s=", 132 | "voteKD": 3163, 133 | "voteLst": 10000000 134 | } 135 | }, 136 | { 137 | "addr": "ULABSGYLHNHMST7PHDE3DZJSJKGPXGBTA7T3E5UW4MDHZOI7YS7LZU7BZI", 138 | "comment": "", 139 | "state": { 140 | "algo": 10000000000, 141 | "onl": 1, 142 | "sel": "AHsPMQDDTCV9tj5Sn/w+gX2PqB7XYA2dN1kkNHV8kBk=", 143 | "stprf": "2B6SYIOGA4hQ418zR3Ak1ixiPbdTXPAO2lIxHGcNe3KVSya2L+PthxqkGRapRwg2imsMPUrKm5/WcKI2r7LzGg==", 144 | "vote": "O0GUgZBuney5kqDnX/pNVTq6jgUGMtfdgM+9u2/ZgAM=", 145 | "voteKD": 3163, 146 | "voteLst": 10000000 147 | } 148 | }, 149 | { 150 | "addr": "NQYHZ2XQRCP5GICPEGOM62545HLPS34Y77DNC3UOA6JHYRXKY2X4NRLM5A", 151 | "comment": "", 152 | "state": { 153 | "algo": 10000000000, 154 | "onl": 1, 155 | "sel": "MS//jcPnT/g785vT7f1ovrZc45hfTxUpuFwQwWM5LnY=", 156 | "stprf": "xb3TPh9H9L9b5OgKw9oOY3eCnfSv+lTpqKaJbydgP3TF9SM1WqSeMNeu3H5It5RcWaCBfSlNZc3trL2p1xcdwA==", 157 | "vote": "E1TMzF/fy6WLceNCx/Dc+GQTMC6akdjMlNVenv9nsWQ=", 158 | "voteKD": 3163, 159 | "voteLst": 10000000 160 | } 161 | }, 162 | { 163 | "addr": "K627B2V4IL3Y7SCGDA3CJTE2LGSHB6V5CKSB3BI3WFR6BMCBFWSQN7BEWQ", 164 | "comment": "", 165 | "state": { 166 | "algo": 10000000000, 167 | "onl": 1, 168 | "sel": "Db97NdrnQEH8KYaQfxw2duJT9d55wCYMdSKvXXwkz1k=", 169 | "stprf": "ja/XQZTWJwgOwm6lstoK7EKuPAdJsMznSULZSySoHn6mIfJ7108g9K2G1ekurRbRE3RIt1KBGwxWIC96BnXfpQ==", 170 | "vote": "tUUL6SrY8YLN2kccBd6dWigfV9k3WOHIRdy/1iouyBs=", 171 | "voteKD": 3163, 172 | "voteLst": 10000000 173 | } 174 | }, 175 | { 176 | "addr": "LEZHUXCTPUCM3WZVQKDP5VZZBJWMRO5KFOJZ27XY7PBCRO4H4I2ICJBWRQ", 177 | "comment": "", 178 | "state": { 179 | "algo": 10000000000, 180 | "onl": 1, 181 | "sel": "dM6pgiwCY+TlU8hEFLLZK/DpmzQW6TcX8kKkr5pWuY8=", 182 | "stprf": "PKEyAWepvucJrJ7tXI52sFTObOJy8GETyEC8rdUE2orxIemlYFrmbqFCJwqz2/j762ZYHUgG83Uj/Jai0DVulA==", 183 | "vote": "/DqVtsEEVL8cIpKRUjXqfjb54xua5zbs4bHDReT+kfk=", 184 | "voteKD": 3163, 185 | "voteLst": 10000000 186 | } 187 | }, 188 | { 189 | "addr": "H5ZIJBAEGQ7MYBVCF4LWLNFUEYUQXZDXVLLVVUBBFHHALYPAZOVSBMEVT4", 190 | "comment": "", 191 | "state": { 192 | "algo": 10000000000, 193 | "onl": 1, 194 | "sel": "IZp4L/7OW3BlvwJSq7E2HTpv9ZAY23+B6Fg9puZtm0M=", 195 | "stprf": "foGHFjxc88FeGs/FBtNvx2uqsxS5kUhMOwG7bj8RzxkamMc8w8G0zCvqbxfvD3/yC+uxYzdDlTusJwsn/pnNog==", 196 | "vote": "wAl7g2ZUrX84y30/uN2oA0iE28uO++KwlHzfXxF1OA0=", 197 | "voteKD": 3163, 198 | "voteLst": 10000000 199 | } 200 | }, 201 | { 202 | "addr": "GKN3EHZTEYAYTN6OW4LQ7RWWLMPPGDGEHVAUWI6S5GDEGIJUMKNNVRS7H4", 203 | "comment": "", 204 | "state": { 205 | "algo": 10000000000, 206 | "onl": 1, 207 | "sel": "On1EpPmVWqM9GyTw2YENwbxhAg2pVoDXggLGD11Kd7c=", 208 | "stprf": "l/kDJSYwtjbt01zICXPPyHRNf8G5H7bSRIUlSTcibpkOMjOBpfzTi+B/OJxtXRaaaz8N6AjgE3QoLS1Ec+vdsA==", 209 | "vote": "Rm8eNUUzbR/bSZHRZoEtB2FyVuqoJe2JIuvXCeTNrm8=", 210 | "voteKD": 3163, 211 | "voteLst": 10000000 212 | } 213 | }, 214 | { 215 | "addr": "NVBQH722R6A7HUYIAQS2O6ZJB7UTOOFM6G4ILYIRPWE3OEOLHBOJR35DAY", 216 | "comment": "", 217 | "state": { 218 | "algo": 2327200000000000 219 | } 220 | }, 221 | { 222 | "addr": "IW5JBGLGVI2KV43ZYJROMYB2UL2MRHSOEN7H2EI4UK2SPA7XOL6KAMSEMU", 223 | "comment": "", 224 | "state": { 225 | "algo": 2000000000000000 226 | } 227 | }, 228 | { 229 | "addr": "WTAUFFJLWIOO2NFNV3ANFNCLX6CQEYRCOD6IEVYN5SJIFQHC76WOEPOR7U", 230 | "comment": "Built by You. Run by You. Owned by You.", 231 | "state": { 232 | "algo": 5672630000000000 233 | } 234 | } 235 | ], 236 | "fees": "TBEIGCNK4UCN3YDP2NODK3MJHTUZMYS3TABRM2MVSI2MPUR2V36E5JYHSY", 237 | "id": "v1.0", 238 | "network": "voimain", 239 | "proto": "https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95", 240 | "rwd": "UVPIYH5R7F7ZOLRPE2SLGISO4C5Q72DWJGKARZOXF4HIL67IDCFLEWLMUA" 241 | } 242 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, session, shell } from 'electron'; 2 | import isDev from 'electron-is-dev'; 3 | import Store from 'electron-persist-secure/lib/store'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | 7 | import { productName } from '../package.json'; 8 | 9 | const DEFAULT_NETWORK = 'algorand.mainnet'; 10 | const DEFAULT_PORT = 4160; 11 | 12 | const NETWORKS = ['algorand.mainnet', 'voi.mainnet']; 13 | 14 | // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack 15 | // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on 16 | // whether you're running in development or production). 17 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string; 18 | declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; 19 | 20 | export type ModifiedBrowserWindow = BrowserWindow & { 21 | getDataDir: () => string; 22 | network: string; 23 | store: Store; 24 | }; 25 | 26 | // Squirrel.Windows will spawn the app multiple times while installing/updating 27 | // to make sure only one app is running, we quit if we detect squirrel 28 | if (require('electron-squirrel-startup')) { 29 | const squirrelEvent = process.argv[1]; 30 | if (squirrelEvent === '--squirrel-uninstall') { 31 | // delete the data directory 32 | const DATA_DIR = path.join(app.getPath('appData'), productName, 'data'); 33 | if (fs.existsSync(DATA_DIR)) { 34 | fs.rmdirSync(DATA_DIR, { recursive: true }); 35 | } 36 | } 37 | 38 | app.quit(); 39 | } 40 | 41 | const firstInstance = app.requestSingleInstanceLock(); 42 | if (!firstInstance) { 43 | // only allow one instance of the app 44 | // we will handle multiple nodes as separate windows 45 | app.quit(); 46 | } 47 | 48 | let storeMap: Record = {}; 49 | const loadStore = (network: string) => { 50 | if (!(network in storeMap)) { 51 | storeMap[network] = new Store({ 52 | configName: network, 53 | }); 54 | } 55 | 56 | const store = storeMap[network]; 57 | store.set('accounts', store.get('accounts', {})); 58 | store.set('darkMode', store.get('darkMode', false)); 59 | store.set( 60 | 'dataDir', 61 | store.get('dataDir') || 62 | path.join(app.getPath('appData'), productName, 'data', network), 63 | ); 64 | store.set('guid', store.get('guid', '')); 65 | store.set('nodeName', store.get('nodeName', '')); 66 | store.set('port', store.get('port', DEFAULT_PORT)); 67 | store.set('startup', store.get('startup', false)); 68 | 69 | return store; 70 | }; 71 | 72 | let mainStore: Store; 73 | const createMainStore = () => { 74 | mainStore = new Store({ 75 | configName: 'main', 76 | }); 77 | 78 | mainStore.set('startupNetworks', mainStore.get('startupNetworks', [])); 79 | 80 | // need to do some migrations for v1.4.0 81 | const hasMigrated_v140 = mainStore.get('hasMigrated_v140', false); 82 | if (!hasMigrated_v140) { 83 | mainStore.set('hasMigrated_v140', true); 84 | 85 | // check to see if the old config exists 86 | const store = new Store({ 87 | configName: 'config', 88 | }); 89 | 90 | const network = store.get('network') as string; 91 | if (network) { 92 | // the old config exists, so we need to migrate 93 | const accounts = store.get('accounts', {}); 94 | const darkMode = store.get('darkMode', false); 95 | const nodeName = store.get('nodeName'); 96 | const port = store.get('port') as number; 97 | const startup = store.get('startup'); 98 | 99 | const firstNetworkStore = loadStore(network); 100 | firstNetworkStore.set('accounts', accounts); 101 | firstNetworkStore.set('darkMode', darkMode); 102 | firstNetworkStore.set('nodeName', nodeName); 103 | firstNetworkStore.set('port', port); 104 | firstNetworkStore.set('startup', startup); 105 | if (startup) { 106 | mainStore.set('startupNetworks', [network]); 107 | } 108 | 109 | const otherNetwork = NETWORKS.find((n) => n !== network); 110 | const secondNetworkStore = loadStore(otherNetwork!); 111 | firstNetworkStore.set('accounts', accounts); 112 | firstNetworkStore.set('darkMode', darkMode); 113 | secondNetworkStore.set('nodeName', ''); 114 | secondNetworkStore.set('port', port + 1); 115 | secondNetworkStore.set('startup', false); 116 | } 117 | } 118 | }; 119 | 120 | let runningNetworks: string[] = []; 121 | const createWindow = (network: string) => { 122 | // make sure we only run one instance of each network 123 | if (runningNetworks.includes(network)) { 124 | return; 125 | } 126 | 127 | // make sure we only start windows for networks we support 128 | if (!NETWORKS.includes(network)) { 129 | return; 130 | } 131 | 132 | const suffix = 133 | process.platform === 'darwin' 134 | ? 'icns' 135 | : process.platform === 'linux' 136 | ? 'png' 137 | : 'ico'; 138 | const icon = `icon.${suffix}`; 139 | 140 | // Create the browser window. 141 | const window = new BrowserWindow({ 142 | frame: false, 143 | height: 720, 144 | icon: app.isPackaged 145 | ? path.join(process.resourcesPath, icon) 146 | : path.join(__dirname, '..', '..', 'src', 'assets', 'icons', icon), 147 | webPreferences: { 148 | preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, 149 | }, 150 | width: 1024, 151 | }) as ModifiedBrowserWindow; 152 | 153 | // and load the index.html of the app. 154 | window.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); 155 | 156 | if (isDev) { 157 | window.webContents.openDevTools({ mode: 'detach' }); 158 | } 159 | 160 | // remove the menu 161 | window.removeMenu(); 162 | window.setWindowButtonVisibility?.(false); 163 | 164 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 165 | callback({ 166 | responseHeaders: { 167 | ...details.responseHeaders, 168 | 'Content-Security-Policy': [ 169 | `connect-src 'self' data: http://localhost:* https://vp2apscqbf2e57yys6x4iczcyi0znuce.lambda-url.us-west-2.on.aws https://api.github.com https://*.defly.app https://*.perawallet.app wss://*.walletconnect.org wss://*.defly.app wss://*.perawallet.app https://g.nodely.io; font-src 'self' https://fonts.gstatic.com; object-src 'none'; script-src 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com`, 170 | ], 171 | }, 172 | }); 173 | }); 174 | 175 | let lastDataDir: string | undefined; 176 | window.getDataDir = () => { 177 | let dataDir = window.store.get('dataDir') as string; 178 | if (dataDir === '') { 179 | dataDir = path.join(app.getPath('appData'), productName, 'data', network); 180 | window.store.set('dataDir', dataDir); 181 | } 182 | 183 | if (lastDataDir !== undefined && lastDataDir !== dataDir) { 184 | // this happens when the user changes the data dir in the settings 185 | // we will just move the old data directory to keep keys, sync, etc. 186 | try { 187 | if (!fs.existsSync(dataDir)) { 188 | fs.mkdirSync(dataDir, { recursive: true, mode: 0o777 }); 189 | } 190 | 191 | if (fs.existsSync(lastDataDir)) { 192 | try { 193 | // try to rename the old directory 194 | fs.renameSync(lastDataDir, dataDir); 195 | } catch (err) { 196 | try { 197 | // try copying the old directory / deleting it 198 | fs.copyFileSync(lastDataDir, dataDir); 199 | fs.rmdirSync(lastDataDir, { recursive: true }); 200 | } catch (err) { 201 | // just start from scratch in the new directory 202 | } 203 | } 204 | } 205 | } catch (err) { 206 | // if we can't create the new directory, we have to revert back 207 | window.store.set('dataDir', lastDataDir); 208 | return lastDataDir; 209 | } 210 | } else if (!fs.existsSync(dataDir)) { 211 | fs.mkdirSync(dataDir, { recursive: true, mode: 0o777 }); 212 | 213 | // we made it so multiple network data dirs can coexist 214 | // v1.0.0 and below used to use the same data dir for all networks 215 | // so we need to move the old data dir to the new one, if applicable 216 | if (dataDir.endsWith('algorand.mainnet')) { 217 | const oldDataDir = path.join( 218 | app.getPath('appData'), 219 | productName, 220 | 'algod', 221 | 'data', 222 | ); 223 | 224 | if (fs.existsSync(oldDataDir)) { 225 | fs.renameSync(oldDataDir, dataDir); 226 | } 227 | } 228 | } 229 | 230 | lastDataDir = dataDir; 231 | return dataDir; 232 | }; 233 | 234 | window.network = network; 235 | window.store = loadStore(network); 236 | 237 | // if we are starting a second network, 238 | // make sure the ports are different 239 | if (runningNetworks.length > 0) { 240 | const prevNetwork = runningNetworks[runningNetworks.length - 1]; 241 | const prevStore = loadStore(prevNetwork); 242 | if (prevStore.get('port') === window.store.get('port')) { 243 | window.store.set('port', (window.store.get('port') as number) + 1); 244 | } 245 | } 246 | 247 | runningNetworks.push(network); 248 | window.on('closed', () => { 249 | runningNetworks = runningNetworks.filter((n) => n !== network); 250 | }); 251 | }; 252 | 253 | // This method will be called when Electron has finished 254 | // initialization and is ready to create browser windows. 255 | // Some APIs can only be used after this event occurs. 256 | app.on('ready', () => { 257 | createMainStore(); 258 | 259 | // create a window for each network in the startup list 260 | const startupNetworks = mainStore.get('startupNetworks') as string[]; 261 | for (const network of startupNetworks) { 262 | createWindow(network); 263 | } 264 | 265 | // make sure at least the default window pops up 266 | if (startupNetworks.length === 0) { 267 | createWindow(DEFAULT_NETWORK); 268 | } 269 | }); 270 | 271 | // Quit when all windows are closed, except on macOS. There, it's common 272 | // for applications and their menu bar to stay active until the user quits 273 | // explicitly with Cmd + Q. 274 | app.on('window-all-closed', () => { 275 | if (process.platform !== 'darwin') { 276 | app.quit(); 277 | } 278 | }); 279 | 280 | app.on('activate', () => { 281 | // On OS X it's common to re-create a window in the app when the 282 | // dock icon is clicked and there are no other windows open. 283 | if (BrowserWindow.getAllWindows().length === 0) { 284 | createWindow( 285 | (mainStore.get('startupNetworks') as string[])[0] || DEFAULT_NETWORK, 286 | ); 287 | } 288 | }); 289 | 290 | // make sure all links open in the default browser 291 | app.on('web-contents-created', (_, contents) => { 292 | contents.on('will-attach-webview', (event) => event.preventDefault()); 293 | contents.on('will-navigate', (event) => event.preventDefault()); 294 | contents.setWindowOpenHandler(({ url }) => { 295 | shell.openExternal(url); 296 | return { action: 'deny' }; 297 | }); 298 | }); 299 | 300 | // IPC handlers 301 | import './bridge/goal'; 302 | 303 | ipcMain.on('isDev', (event) => event.sender.send('isDev', null, isDev)); 304 | 305 | ipcMain.on('loadConfig', (event) => { 306 | const window = BrowserWindow.fromWebContents( 307 | event.sender, 308 | )! as ModifiedBrowserWindow; 309 | 310 | event.sender.send('loadConfig', null, { 311 | dataDir: window.store.get('dataDir'), 312 | guid: window.store.get('guid'), 313 | network: window.network, 314 | port: window.store.get('port'), 315 | }); 316 | }); 317 | 318 | ipcMain.on('maximize', (event) => { 319 | BrowserWindow.fromWebContents(event.sender)?.maximize(); 320 | event.sender.send('maximize'); 321 | }); 322 | 323 | ipcMain.on('maximized', (event) => { 324 | event.sender.send( 325 | 'maximized', 326 | null, 327 | BrowserWindow.fromWebContents(event.sender)?.isMaximized(), 328 | ); 329 | }); 330 | 331 | ipcMain.on('minimize', (event) => { 332 | if (process.platform === 'darwin') { 333 | app.hide(); 334 | } else { 335 | BrowserWindow.fromWebContents(event.sender)?.minimize(); 336 | } 337 | 338 | event.sender.send('minimize'); 339 | }); 340 | 341 | ipcMain.on('newWindow', (event, { network }) => { 342 | createWindow(network); 343 | event.sender.send('newWindow'); 344 | }); 345 | 346 | ipcMain.on('platform', (event) => { 347 | event.sender.send('platform', null, process.platform); 348 | }); 349 | 350 | ipcMain.on('quit', (event) => { 351 | BrowserWindow.fromWebContents(event.sender)?.close(); 352 | }); 353 | 354 | ipcMain.on('refresh', (event) => { 355 | const window = BrowserWindow.fromWebContents( 356 | event.sender, 357 | )! as ModifiedBrowserWindow; 358 | 359 | window.close(); 360 | createWindow(window.network); 361 | }); 362 | 363 | ipcMain.on('setStartup', (event, { startup }) => { 364 | const window = BrowserWindow.fromWebContents( 365 | event.sender, 366 | )! as ModifiedBrowserWindow; 367 | 368 | let startupNetworks = mainStore.get('startupNetworks') as string[]; 369 | if (startup && !startupNetworks.includes(window.network)) { 370 | startupNetworks.push(window.network); 371 | mainStore.set('startupNetworks', startupNetworks); 372 | } 373 | if (!startup && startupNetworks.includes(window.network)) { 374 | startupNetworks = startupNetworks.filter((n) => n !== window.network); 375 | mainStore.set('startupNetworks', startupNetworks); 376 | } 377 | 378 | // even though we specify .exe for windows, the args that use them 379 | // are for windows only, so the settings still work for macos/linux 380 | const appFolder = path.dirname(process.execPath); 381 | const updateExe = path.resolve(appFolder, '..', 'Update.exe'); 382 | const exeName = path.basename(process.execPath); 383 | 384 | app.setLoginItemSettings({ 385 | args: ['--processStart', `"${exeName}"`], 386 | openAtLogin: startupNetworks.length > 0, 387 | path: updateExe, 388 | }); 389 | 390 | window.store.set('startup', startup); 391 | event.sender.send('setStartup'); 392 | }); 393 | 394 | ipcMain.on('swapNetwork', (event, { network }) => { 395 | const window = BrowserWindow.fromWebContents( 396 | event.sender, 397 | )! as ModifiedBrowserWindow; 398 | 399 | window.network = network; 400 | window.store = loadStore(network); 401 | 402 | event.sender.send('swapNetwork'); 403 | }); 404 | 405 | ipcMain.on('unmaximize', (event) => { 406 | BrowserWindow.fromWebContents(event.sender)?.unmaximize(); 407 | event.sender.send('unmaximize'); 408 | }); 409 | -------------------------------------------------------------------------------- /src/preload.ts: -------------------------------------------------------------------------------- 1 | // See the Electron documentation for details on how to use preload scripts: 2 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 3 | 4 | import { contextBridge, ipcRenderer } from 'electron'; 5 | import { createStoreBindings } from 'electron-persist-secure/lib/bindings'; 6 | 7 | // we keep a registry of all pending IPC events with their corresponding callbacks 8 | let ipcRegistry: Record = {}; 9 | function sendIPC( 10 | eventName: string, 11 | { stderr, stdout, ...options }: any = {}, 12 | ) { 13 | return new Promise((resolve, reject) => { 14 | // make sure we only ever have one listener for each event 15 | if (!ipcRegistry[eventName]) { 16 | ipcRenderer.on(eventName, (_: any, err: any, result: any) => 17 | ipcRegistry[eventName](err, result), 18 | ); 19 | 20 | // handle buffers 21 | ipcRenderer.on( 22 | `${eventName}.stderr`, 23 | (_: any, err: any) => 24 | ipcRegistry[eventName].stderr && ipcRegistry[eventName].stderr(err), 25 | ); 26 | ipcRenderer.on( 27 | `${eventName}.stdout`, 28 | (_: any, __: any, result: any) => 29 | ipcRegistry[eventName].stdout && 30 | ipcRegistry[eventName].stdout(result), 31 | ); 32 | } else if (!ipcRegistry[eventName].fired) { 33 | // if the event has been registered but not fired, reject the previous promise 34 | ipcRegistry[eventName].reject( 35 | new Error(`Event ${eventName} was re-registered before it was fired.`), 36 | ); 37 | } 38 | 39 | ipcRegistry[eventName] = (err: any, result: any) => { 40 | ipcRegistry[eventName].fired = true; 41 | if (err) { 42 | reject(err); 43 | } else { 44 | resolve(result); 45 | } 46 | }; 47 | 48 | ipcRegistry[eventName].fired = false; 49 | ipcRegistry[eventName].reject = reject; 50 | ipcRegistry[eventName].stderr = stderr; 51 | ipcRegistry[eventName].stdout = stdout; 52 | 53 | ipcRenderer.send(eventName, options); 54 | }); 55 | } 56 | 57 | const electron = { 58 | isDev: () => sendIPC('isDev'), 59 | loadConfig: async () => { 60 | const { dataDir, guid, network, port } = await sendIPC('loadConfig'); 61 | return { 62 | dataDir, 63 | guid, 64 | network, 65 | port, 66 | store: createStoreBindings(network), 67 | }; 68 | }, 69 | maximize: () => sendIPC('maximize'), 70 | maximized: () => sendIPC('maximized'), 71 | minimize: () => sendIPC('minimize'), 72 | newWindow: (network: string) => sendIPC('newWindow', { network }), 73 | platform: () => sendIPC('platform'), 74 | quit: () => sendIPC('quit'), 75 | refresh: () => sendIPC('refresh'), 76 | setStartup: (startup: boolean) => sendIPC('setStartup', { startup }), 77 | swapNetwork: (network: string) => sendIPC('swapNetwork', { network }), 78 | unmaximize: () => sendIPC('unmaximize'), 79 | }; 80 | 81 | const goal = { 82 | addpartkey: ( 83 | { account, firstValid, lastValid } = { 84 | account: '', 85 | firstValid: 0, 86 | lastValid: 0, 87 | }, 88 | ) => 89 | sendIPC('goal.addpartkey', { 90 | account, 91 | firstValid, 92 | lastValid, 93 | }), 94 | catchpoint: () => sendIPC('goal.catchpoint'), 95 | catchup: (catchpoint: string) => sendIPC('goal.catchup', { catchpoint }), 96 | deletepartkey: (id: string) => sendIPC('goal.deletepartkey', { id }), 97 | running: () => sendIPC('goal.running'), 98 | start: () => sendIPC('goal.start'), 99 | status: () => sendIPC('goal.status'), 100 | stop: () => sendIPC('goal.stop'), 101 | telemetry: (nodeName: string, network: string) => 102 | sendIPC('goal.telemetry', { network, nodeName }), 103 | token: () => sendIPC('goal.token'), 104 | }; 105 | 106 | contextBridge.exposeInMainWorld('electron', electron); 107 | contextBridge.exposeInMainWorld('goal', goal); 108 | 109 | declare global { 110 | interface Window { 111 | electron: typeof electron; 112 | goal: typeof goal; 113 | store: ReturnType; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/render/App.tsx: -------------------------------------------------------------------------------- 1 | import Body from '@components/app/Body'; 2 | import Header from '@components/app/Header'; 3 | 4 | import './flux'; 5 | import './index.css'; 6 | 7 | export default function App() { 8 | return ( 9 |
10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Column.tsx: -------------------------------------------------------------------------------- 1 | export default function Column({ 2 | children, 3 | className = '', 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) { 8 | return ( 9 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Dashboard/AccountSelector.tsx: -------------------------------------------------------------------------------- 1 | import flux from '@aust/react-flux'; 2 | import { useWallet } from '@txnlab/use-wallet'; 3 | import algosdk from 'algosdk'; 4 | import Dropdown from 'algoseas-libs/build/react/Dropdown'; 5 | import { useCallback, useEffect, useState } from 'react'; 6 | 7 | import Button from '@components/shared/Button'; 8 | import CheckIcon from '@components/icons/Check'; 9 | import EyeIcon from '@components/icons/Eye'; 10 | import TextInput from '@components/shared/TextInput'; 11 | import WalletIcon from '@components/icons/Wallet'; 12 | 13 | // include the string 'hidden' so tailwind will not purge this class 14 | // the Dropdown component uses it 15 | ('hidden'); 16 | 17 | let fetchedAccounts: Record = {}; 18 | 19 | export default function AccountSelector({ 20 | className = '', 21 | disabled = false, 22 | selectedAccount, 23 | setSelectedAccount, 24 | }: { 25 | className?: string; 26 | disabled?: boolean; 27 | selectedAccount: string; 28 | setSelectedAccount: (account: string) => void; 29 | }) { 30 | const accounts = flux.accounts.useState('list'); 31 | 32 | const { providers, connectedAccounts } = useWallet(); 33 | 34 | useEffect(() => { 35 | for (const account of connectedAccounts) { 36 | if ( 37 | !accounts.includes(account.address) && 38 | !fetchedAccounts[account.address] 39 | ) { 40 | fetchedAccounts[account.address] = true; 41 | flux.dispatch('accounts/add', account.address); 42 | } 43 | } 44 | }, [accounts, connectedAccounts]); 45 | 46 | const connectedAccountsList = connectedAccounts.map( 47 | (account) => account.address, 48 | ); 49 | 50 | const [open, setOpen] = useState(false); 51 | 52 | const close = useCallback(() => { 53 | setOpen(false); 54 | setIsAdding(false); 55 | setNewAccount(''); 56 | }, []); 57 | 58 | const setSelectedAccountAndClose = useCallback( 59 | (account: string) => { 60 | for (const provider of providers || []) { 61 | if (provider.accounts.find((a) => a.address === account)) { 62 | provider.setActiveAccount(account); 63 | break; 64 | } 65 | } 66 | 67 | setSelectedAccount(account); 68 | close(); 69 | }, 70 | [providers], 71 | ); 72 | 73 | const [isAdding, setIsAdding] = useState(false); 74 | const [newAccount, setNewAccount] = useState(''); 75 | 76 | const newAccountIsValid = algosdk.isValidAddress(newAccount); 77 | const addNewAccount = useCallback(() => { 78 | flux.dispatch('accounts/add', newAccount); 79 | setNewAccount(''); 80 | setIsAdding(false); 81 | 82 | setSelectedAccountAndClose(newAccount); 83 | }, [newAccount]); 84 | 85 | useEffect(() => { 86 | if (!selectedAccount && accounts.length > 0) { 87 | setSelectedAccountAndClose(accounts[0]); 88 | } 89 | }, [accounts, selectedAccount]); 90 | 91 | return ( 92 | ( 96 |
97 | {providers?.map((provider) => ( 98 |
102 |
103 |
104 | {`${provider.metadata.name} 109 |
{provider.metadata.name}
110 |
111 | 120 |
121 | {provider.accounts.map((account) => ( 122 |
setSelectedAccountAndClose(account.address)} 128 | > 129 |
139 |
140 | {account.address.substring(0, 6)}... 141 | {account.address.substring(account.address.length - 4)} 142 |
143 |
144 | 149 |
150 | ))} 151 |
152 | ))} 153 |
154 |
155 |
156 |
157 | 158 |
159 |
Watch
160 |
161 | {!isAdding && ( 162 | 163 | )} 164 |
165 | {accounts 166 | .filter((account) => !connectedAccountsList.includes(account)) 167 | .map((account) => ( 168 |
setSelectedAccountAndClose(account)} 174 | > 175 |
182 |
183 | {account.substring(0, 6)}... 184 | {account.substring(account.length - 4)} 185 |
186 |
187 | 192 |
193 | ))} 194 | {isAdding && ( 195 |
196 | 0 199 | ? '[&_input]:!border-red-500' 200 | : '' 201 | } grow`} 202 | onChange={setNewAccount} 203 | onKeyUp={(e) => { 204 | if (e.key === 'Enter' && newAccountIsValid) { 205 | addNewAccount(); 206 | } 207 | }} 208 | placeholder="Enter account address" 209 | type="text" 210 | value={newAccount} 211 | /> 212 | 219 |
220 | )} 221 |
222 |
223 | )} 224 | > 225 | 244 | 245 | ); 246 | } 247 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Dashboard/AccountViewer.tsx: -------------------------------------------------------------------------------- 1 | import flux from '@aust/react-flux'; 2 | import { useWallet } from '@txnlab/use-wallet'; 3 | import { TransactionGroup } from 'algoseas-libs/build/algo'; 4 | import Dropdown from 'algoseas-libs/build/react/Dropdown'; 5 | import { useCallback, useState } from 'react'; 6 | 7 | import Button from '@components/shared/Button'; 8 | import CopySnippet from '@components/shared/CopySnippet'; 9 | import Error from '@components/shared/Error'; 10 | import GearIcon from '@components/icons/Gear'; 11 | import HotAirBalloonIcon from '@components/icons/HotAirBalloon'; 12 | import KeyIcon from '@components/icons/Key'; 13 | import Spinner from '@components/shared/Spinner'; 14 | import { formatNumber } from '@/render/utils'; 15 | 16 | import AccountSelector from './AccountSelector'; 17 | import StatNumber from './StatNumber'; 18 | 19 | const EXPIRING_KEYS_THRESHOLD = 268800; // about two week's worth of blocks 20 | const MIN_ALGO_AMOUNT_FOR_REWARDS_IN_UA = 30_002_000_000; // 30k ALGO + 2 ALGO fee 21 | const PARTICIPATION_PERIOD = 3000000; // about 3 months worth of blocks 22 | const REWARDS_FEE_N = 2000; // 2 ALGO fee required when going online for rewards 23 | const SIGNING_TIMEOUT = 30000; 24 | const STATS_URL = 25 | 'https://vp2apscqbf2e57yys6x4iczcyi0znuce.lambda-url.us-west-2.on.aws/'; 26 | 27 | const PARTICIPATING_NOTE = new Uint8Array( 28 | Array.from("Participating from Aust's One-Click Node.", (c) => 29 | c.charCodeAt(0), 30 | ), 31 | ); 32 | 33 | export default function AccountViewer({ 34 | className = '', 35 | lastBlock, 36 | selectedAccount, 37 | setSelectedAccount, 38 | }: { 39 | className?: string; 40 | lastBlock: number; 41 | selectedAccount: string; 42 | setSelectedAccount: (account: string) => void; 43 | }) { 44 | const { connectedAccounts, signTransactions } = useWallet(); 45 | const [generatingKeys, setGeneratingKeys] = useState(false); 46 | const [generationError, setGenerationError] = useState(''); 47 | const [waitingFor, setWaitingFor] = useState<'' | 'signature' | 'submission'>( 48 | '', 49 | ); 50 | const [submissionError, setSubmissionError] = useState(''); 51 | 52 | const account = flux.accounts.selectState('get', selectedAccount); 53 | const hasKeys = account?.nodeParticipation.selectionKey !== undefined; 54 | const participating = flux.accounts.selectState( 55 | 'participating', 56 | selectedAccount, 57 | ); 58 | const sameKeys = 59 | account?.chainParticipation.voteKey === account?.nodeParticipation.voteKey; 60 | const canRemove = !generatingKeys && !(participating && sameKeys); 61 | const accountConnected = connectedAccounts 62 | .map((account) => account.address) 63 | .includes(selectedAccount); 64 | const keysExpiringSoon = 65 | (account?.nodeParticipation.voteLast || 0) - lastBlock < 66 | EXPIRING_KEYS_THRESHOLD; 67 | const eligibleForRewards = 68 | account?.algoAmount > MIN_ALGO_AMOUNT_FOR_REWARDS_IN_UA; 69 | 70 | const generateKeys = useCallback(async () => { 71 | setGenerationError(''); 72 | setGeneratingKeys(true); 73 | 74 | try { 75 | await window.goal.addpartkey({ 76 | account: selectedAccount, 77 | firstValid: lastBlock, 78 | lastValid: lastBlock + PARTICIPATION_PERIOD, 79 | }); 80 | 81 | // if we re-add the account, it will load the key information 82 | await flux.dispatch('accounts/add', selectedAccount); 83 | 84 | const account = flux.accounts.selectState('get', selectedAccount); 85 | fetch(STATS_URL, { 86 | body: JSON.stringify({ 87 | address: selectedAccount, 88 | key: account.nodeParticipation.voteKey, 89 | type: 'keygen', 90 | }), 91 | method: 'POST', 92 | }); 93 | } catch (err) { 94 | setGenerationError(err.toString()); 95 | } 96 | 97 | setGeneratingKeys(false); 98 | }, [lastBlock, selectedAccount]); 99 | 100 | const signAndSubmit = useCallback( 101 | async (group: TransactionGroup) => { 102 | try { 103 | setSubmissionError(''); 104 | setWaitingFor('signature'); 105 | await group.makeTxns(); 106 | 107 | const txns = await Promise.race([ 108 | signTransactions(group.toUint8Array(), undefined, false), 109 | new Promise((_, reject) => 110 | setTimeout(() => reject('Signing timed out.'), SIGNING_TIMEOUT), 111 | ) as Promise, 112 | ]); 113 | group.storeSignatures(txns); 114 | 115 | setWaitingFor('submission'); 116 | 117 | await Promise.race([ 118 | group.submit(), 119 | new Promise((_, reject) => 120 | setTimeout(() => reject('Submission timed out.'), SIGNING_TIMEOUT), 121 | ) as Promise, 122 | ]); 123 | 124 | // re-load the account to get the new key information 125 | flux.dispatch('accounts/add', selectedAccount); 126 | } catch (err) { 127 | setSubmissionError(err.toString()); 128 | } 129 | 130 | setWaitingFor(''); 131 | }, 132 | [selectedAccount, signTransactions], 133 | ); 134 | 135 | return ( 136 |
139 |
140 | 145 | {account && ( 146 | <> 147 |
148 |
153 |
{!participating && 'Not '}Participating
154 |
155 |
156 | ( 160 |
161 |
164 | flux.dispatch('accounts/reset-stats', selectedAccount) 165 | } 166 | > 167 | Reset Stats 168 |
169 |
{ 176 | if (!generatingKeys) { 177 | generateKeys(); 178 | } 179 | }} 180 | > 181 | {hasKeys ? 'Re-' : ''}Generate Keys 182 |
183 | {hasKeys && ( 184 |
{ 191 | if (!participating || !sameKeys) { 192 | await window.goal.deletepartkey( 193 | account.nodeParticipation.id!, 194 | ); 195 | // re-load the account to get the new key information 196 | flux.dispatch('accounts/add', selectedAccount); 197 | } 198 | }} 199 | > 200 | Remove Keys 201 |
202 | )} 203 |
{ 210 | if (canRemove) { 211 | await flux.dispatch('accounts/remove', selectedAccount); 212 | setSelectedAccount(''); 213 | } 214 | }} 215 | > 216 | Remove Account 217 |
218 |
219 | )} 220 | > 221 | 222 |
223 | 224 | )} 225 |
226 | {!account ? ( 227 |
228 | 229 |
230 | Connect an account to get started. 231 |
232 |
233 | ) : ( 234 | <> 235 |
236 |
237 | 244 | 0 249 | ? formatNumber(account.stats.lastProposedBlock) 250 | : 'N/A' 251 | } 252 | /> 253 | 258 | 263 |
264 |
265 | {!hasKeys || generatingKeys ? ( 266 |
267 |
268 | 269 | 270 | 271 |
272 |
273 |
274 | {generatingKeys ? ( 275 |
276 | 277 |
Generating keys...
278 |
279 | ) : ( 280 |
No keys have been generated for this account.
281 | )} 282 |
283 | Generating keys takes about five minutes. These keys are 284 | used only for consensus, and are incapable of spending 285 | funds. 286 |
287 |
288 | 297 | {generationError && ( 298 | 299 | Failed to generate keys: {generationError} 300 | 301 | )} 302 |
303 |
304 | ) : ( 305 |
306 |
307 |
Vote Key
308 | 309 | {account.nodeParticipation.voteKey} 310 | 311 |
Selection Key
312 | 313 | {account.nodeParticipation.selectionKey} 314 | 315 |
State Proof Key
316 | 317 | {account.nodeParticipation.stateProofKey} 318 | 319 |
320 |
321 | 326 | 331 | 338 |
339 | {!sameKeys && participating ? ( 340 |
341 | This account is participating with different keys than the 342 | ones shown here. 343 |
344 | ) : ( 345 | keysExpiringSoon && ( 346 |
347 | 348 |
349 | Renew your keys to continue participating. 350 |
351 |
352 | ) 353 | )} 354 |
355 | )} 356 |
357 |
358 |
359 | {hasKeys && (!participating || !sameKeys) ? ( 360 | 409 | ) : ( 410 | participating && ( 411 | 436 | ) 437 | )} 438 | {(participating || hasKeys) && !accountConnected && ( 439 |
440 | Connect your account to issue transactions. 441 |
442 | )} 443 | {submissionError ? ( 444 | 445 | {submissionError} 446 | 447 | ) : ( 448 | hasKeys && 449 | accountConnected && ( 450 |
451 | It takes 320 rounds to go online and offline. Plan 452 | accordingly. 453 |
454 | ) 455 | )} 456 |
457 | 458 | )} 459 |
460 | ); 461 | } 462 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Dashboard/StatNumber.tsx: -------------------------------------------------------------------------------- 1 | export default function StatNumber({ 2 | label, 3 | className = '', 4 | small = false, 5 | stat, 6 | }: { 7 | label: React.ReactNode; 8 | className?: string; 9 | small?: boolean; 10 | stat: React.ReactNode; 11 | }) { 12 | return ( 13 |
18 |
{stat}
19 |
24 | {label} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import flux from '@aust/react-flux'; 2 | import algosdk from 'algosdk'; 3 | import { nodeRequest } from 'algoseas-libs/build/algo'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | import AntennaIcon from '@components/icons/Antenna'; 7 | import { Step } from '@/render/flux/wizardStore'; 8 | import { abbreviateNumber, formatNumber } from '@/render/utils'; 9 | 10 | import AccountViewer from './AccountViewer'; 11 | import StatNumber from './StatNumber'; 12 | 13 | const HEALTH_INTERVAL = 15000; // if we haven't seen a block for 15 seconds, do a health check 14 | const PARTICIPATION_INTERVAL = 10; // every 10 blocks we will check for participation 15 | 16 | export default function Dashboard() { 17 | const [lastBlock, setLastBlock] = useState(0); 18 | const [selectedAccount, setSelectedAccount] = useState(''); 19 | 20 | const accounts = flux.accounts.useState('list'); 21 | const totalProposals = flux.accounts.selectState('totalProposals'); 22 | const totalStake = flux.accounts.selectState('totalStake'); 23 | const totalVotes = flux.accounts.selectState('totalVotes'); 24 | 25 | useEffect(() => { 26 | let terminated = false; 27 | 28 | async function fetchBlockAfter(blockNumber: number) { 29 | let timeout = setTimeout(() => { 30 | if (flux.wizard.selectState('currentStep') === Step.Dashboard) { 31 | flux.dispatch('wizard/checkNodeRunning'); 32 | } 33 | }, HEALTH_INTERVAL); 34 | 35 | try { 36 | const status = await nodeRequest( 37 | `/v2/status/wait-for-block-after/${blockNumber}`, 38 | { fetchTimeoutMs: HEALTH_INTERVAL, maxRetries: 0 }, 39 | ); 40 | clearTimeout(timeout); 41 | if (terminated) { 42 | return; 43 | } 44 | 45 | const lastRound = status['last-round']; 46 | const response = await nodeRequest( 47 | `/v2/blocks/${lastRound}?format=msgpack`, 48 | ); 49 | 50 | const accounts = flux.accounts.selectState('list'); 51 | const proposer = algosdk.encodeAddress(response.cert.prop.oprop); 52 | // check if any of our accounts were the proposer 53 | for (const account of accounts) { 54 | if (account === proposer) { 55 | flux.dispatch('accounts/stats/addProposal', account, lastRound); 56 | break; 57 | } 58 | } 59 | 60 | for (const vote of response.cert.vote) { 61 | const voter = algosdk.encodeAddress(vote.snd); 62 | // check if any of our accounts were voters 63 | for (const account of accounts) { 64 | if (account === voter) { 65 | flux.dispatch('accounts/stats/addVote', account); 66 | break; 67 | } 68 | } 69 | } 70 | 71 | // every 10 blocks, check all of our accounts for participation 72 | if (lastRound % PARTICIPATION_INTERVAL === 0) { 73 | flux.dispatch('accounts/refresh'); 74 | } 75 | 76 | setLastBlock(lastRound); 77 | fetchBlockAfter(lastRound); 78 | } catch (err) { 79 | // any errors means the node is not running 80 | // our health check will fix it, so just ignore 81 | console.error(err); 82 | } 83 | } 84 | 85 | fetchBlockAfter(0); 86 | 87 | // also refresh the accounts 88 | flux.dispatch('accounts/refresh'); 89 | 90 | return () => void (terminated = true); 91 | }, []); 92 | 93 | return ( 94 |
95 |
96 | 97 | 101 | 102 | 103 | 107 | 108 |
109 | 114 |
115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Flush.tsx: -------------------------------------------------------------------------------- 1 | export default function Flush({ 2 | children, 3 | className = '', 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) { 8 | return ( 9 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/render/components/app/Body/Settings.tsx: -------------------------------------------------------------------------------- 1 | import flux from '@aust/react-flux'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import Button from '@components/shared/Button'; 5 | import Checkbox from '@components/shared/Checkbox'; 6 | import CopySnippet from '@components/shared/CopySnippet'; 7 | import Select from '@components/shared/Select'; 8 | import Spinner from '@components/shared/Spinner'; 9 | import TextInput from '@components/shared/TextInput'; 10 | import { parseNumber } from '@/render/utils'; 11 | 12 | import Flush from './Flush'; 13 | 14 | let initialDataDir = ''; 15 | export default function Settings({ className = '' }: { className?: string }) { 16 | const [dataDir, setDataDir] = useState(''); 17 | const [network, setNetwork] = useState(''); 18 | const [nodeName, setNodeName] = useState(''); 19 | const [port, setPort] = useState(''); 20 | const [startup, setStartup] = useState(false); 21 | const [settingDataDir, setSettingDataDir] = useState(false); 22 | const [settingNetwork, setSettingNetwork] = useState(false); 23 | const [settingPort, setSettingPort] = useState(false); 24 | const [settingTelemetry, setSettingTelemetry] = useState(false); 25 | const [telemetryEnabled, setTelemetryEnabled] = useState(false); 26 | const [token, setToken] = useState(''); 27 | 28 | const networks = flux.wizard.selectState('networks'); 29 | const otherNetwork = networks.find( 30 | (n) => n.value !== flux.wizard.selectState('network'), 31 | )!; 32 | 33 | const infraHash = flux.wizard.useState('infraHash'); 34 | 35 | useEffect(() => { 36 | (async () => { 37 | const dataDir = (await window.store.get('dataDir')) as string; 38 | const network = flux.wizard.selectState('network'); 39 | const nodeName = (await window.store.get('nodeName')) as string; 40 | const port = (await window.store.get('port')) as number; 41 | const startup = (await window.store.get('startup')) as boolean; 42 | const token = (await window.goal.token()) as string; 43 | 44 | initialDataDir = dataDir; 45 | 46 | setDataDir(dataDir); 47 | setNetwork(network); 48 | setNodeName(nodeName); 49 | setPort(port.toString()); 50 | setStartup(startup); 51 | setTelemetryEnabled(nodeName !== ''); 52 | setToken(token); 53 | })(); 54 | 55 | // if a bad data directory is entered (i.e. permissions issue) 56 | // electron will reset the dataDir in the store to the last known good one 57 | // this code will detect that and update the UI to match 58 | let interval = window.setInterval(async () => { 59 | const dataDir = (await window.store.get('dataDir')) as string; 60 | if (dataDir !== initialDataDir) { 61 | flux.dispatch('wizard/setDataDir', dataDir); 62 | window.clearInterval(interval); 63 | } 64 | }, 1000); 65 | 66 | return () => window.clearInterval(interval); 67 | }, [infraHash]); 68 | 69 | return ( 70 | 71 |
Settings
72 |
73 | Note: Changing most settings will cause the node to restart. 74 |
75 | { 80 | await window.electron.setStartup(checked); 81 | setStartup(checked); 82 | }} 83 | /> 84 |
85 |
Port
86 |
87 | setPort(value)} 90 | type="number" 91 | value={port} 92 | /> 93 | 119 |
120 |
121 |
122 |
Network
123 |
124 | onChange(e.target.checked)} 20 | type="checkbox" 21 | /> 22 | 27 | {label && ( 28 |
33 | {label} 34 |
35 | )} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/render/components/shared/Console.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export default function Console({ 4 | children, 5 | className = '', 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }) { 10 | const ref = useRef(null); 11 | if (ref.current) { 12 | ref.current.scrollTop = ref.current.scrollHeight; 13 | } 14 | 15 | return ( 16 |
20 | {children} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/render/components/shared/CopySnippet.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import ClipboardIcon from '@components/icons/Clipboard'; 4 | 5 | import Tooltip from './Tooltip'; 6 | 7 | export default function CopySnippet({ 8 | children, 9 | className = '', 10 | }: { 11 | children: React.ReactNode; 12 | className?: string; 13 | }) { 14 | const [showTooltip, setShowTooltip] = useState(false); 15 | 16 | return ( 17 |
18 |
19 |         {children}
20 |       
21 | 26 |
{ 29 | navigator.clipboard.writeText( 30 | Array.isArray(children) 31 | ? children.join('') 32 | : (children as string), 33 | ); 34 | setShowTooltip(true); 35 | setTimeout(() => setShowTooltip(false), 1500); 36 | }} 37 | > 38 | 39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/render/components/shared/Error.tsx: -------------------------------------------------------------------------------- 1 | export default function Error({ 2 | children, 3 | className = '', 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) { 8 | return ( 9 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/render/components/shared/Link.tsx: -------------------------------------------------------------------------------- 1 | export default function Link({ 2 | children, 3 | className = '', 4 | href, 5 | }: { 6 | children?: React.ReactNode; 7 | className?: string; 8 | href: string; 9 | }) { 10 | return ( 11 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/render/components/shared/Select.tsx: -------------------------------------------------------------------------------- 1 | export default function Select({ 2 | className = '', 3 | items, 4 | onChange, 5 | value, 6 | }: { 7 | className?: string; 8 | items: { label: string; value: string }[]; 9 | onChange: (value: string) => void; 10 | value: string; 11 | }) { 12 | return ( 13 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/render/components/shared/Spinner.tsx: -------------------------------------------------------------------------------- 1 | export default function Spinner({ className = '' }: { className?: string }) { 2 | return ( 3 | 9 | 17 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/render/components/shared/Success.tsx: -------------------------------------------------------------------------------- 1 | export default function Success({ 2 | children, 3 | className = '', 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) { 8 | return ( 9 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/render/components/shared/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import { parseNumber } from '@/render/utils'; 2 | 3 | export default function TextInput({ 4 | className = '', 5 | icon = '', 6 | label = '', 7 | max = '', 8 | min = '', 9 | multiline = false, 10 | onChange, 11 | onKeyUp = undefined, 12 | placeholder = '', 13 | required = false, 14 | step = 'any', 15 | type = 'text', 16 | value, 17 | }: { 18 | className?: string; 19 | icon?: React.ReactNode; 20 | label?: string; 21 | max?: string; 22 | min?: string; 23 | multiline?: boolean; 24 | onChange: (value: string) => void; 25 | onKeyUp?: (e: React.KeyboardEvent) => void; 26 | placeholder?: string; 27 | required?: boolean; 28 | step?: string; 29 | type?: string; 30 | value: string; 31 | }) { 32 | const Element = multiline ? 'textarea' : 'input'; 33 | 34 | return ( 35 |
36 | {}} 43 | required={required} 44 | step={step} 45 | tabIndex={-1} 46 | type={type} 47 | value={type === 'number' ? parseNumber(value) || '' : value} 48 | /> 49 | {label && ( 50 |
input:invalid~&]:text-red-500 text-slate-500 text-sm`} 52 | > 53 | {label} 54 |
55 | )} 56 |
57 | input:invalid~div>&]:border-red-500 order-1 focus:outline-none peer px-4 py-2 placeholder:text-slate-500 ${ 59 | icon ? 'rounded-r' : 'rounded' 60 | } focus:shadow transition w-full`} 61 | onChange={(e) => onChange(e.target.value)} 62 | onKeyUp={onKeyUp} 63 | placeholder={placeholder} 64 | required={required} 65 | rows={4} 66 | type="text" 67 | value={value} 68 | /> 69 | {icon && ( 70 |
input:invalid~div>&]:bg-red-500 peer-focus:bg-sky-600 inline-block leading-none p-3 rounded-l peer-focus:shadow transition w-10`} 72 | > 73 | {icon} 74 |
75 | )} 76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/render/components/shared/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import Dropdown from 'algoseas-libs/build/react/Dropdown'; 4 | 5 | export default function Tooltip({ 6 | children, 7 | className = '', 8 | open = undefined, 9 | tooltip, 10 | }: { 11 | children: React.ReactNode; 12 | className?: string; 13 | open?: boolean; 14 | tooltip: React.ReactNode; 15 | }) { 16 | const [_open, setOpen] = useState(false); 17 | if (open === undefined) { 18 | open = _open; 19 | } 20 | 21 | const [opacity, setOpacity] = useState('opacity-0'); 22 | useEffect(() => { 23 | if (open) { 24 | setOpacity('opacity-100'); 25 | } else { 26 | setOpacity('opacity-0'); 27 | } 28 | }, [open, setOpacity]); 29 | 30 | return ( 31 | ( 38 |
43 |
50 |
{tooltip}
51 |
52 | )} 53 | > 54 |
setOpen(true)} 56 | onMouseLeave={() => open && setOpen(false)} 57 | > 58 | {children} 59 |
60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/render/flux/accountsStore.ts: -------------------------------------------------------------------------------- 1 | import flux, { Store } from '@aust/react-flux'; 2 | import algosdk from 'algosdk'; 3 | import { nodeRequest } from 'algoseas-libs/build/algo'; 4 | import { produce } from 'immer'; 5 | 6 | const VOTE_SAVE_INTERVAL = 60_000; // 30 seconds 7 | 8 | type ParticipationDetails = { 9 | id?: string; 10 | selectionKey?: string; 11 | stateProofKey?: string; 12 | voteFirst?: number; 13 | voteKey?: string; 14 | voteKeyDilution?: number; 15 | voteLast?: number; 16 | }; 17 | 18 | type Account = { 19 | address: string; 20 | algoAmount: number; 21 | chainParticipation: ParticipationDetails; 22 | nodeParticipation: ParticipationDetails; 23 | pk: Uint8Array; 24 | stats: { 25 | lastProposedBlock: number; 26 | proposals: number; 27 | votes: number; 28 | }; 29 | }; 30 | 31 | type AccountsStoreState = { 32 | accounts: Record; 33 | 34 | // selectors 35 | anyParticipating: boolean; 36 | get: Account; 37 | list: string[]; 38 | participating: boolean; 39 | totalProposals: number; 40 | totalStake: number; 41 | totalVotes: number; 42 | }; 43 | 44 | declare global { 45 | interface Flux { 46 | accounts: Store; 47 | } 48 | } 49 | 50 | const store = flux.addStore('accounts', { 51 | accounts: {}, 52 | }) as any as Store; 53 | 54 | store.register('node/ready', () => void flux.dispatch('accounts/load')); 55 | 56 | store.register('accounts/load', async () => { 57 | const accounts = (await window.store.get('accounts', {})) as Record< 58 | string, 59 | Account 60 | >; 61 | 62 | // reset node participation so we can always use the latest information 63 | for (const address of Object.keys(accounts)) { 64 | accounts[address].nodeParticipation = {}; 65 | } 66 | 67 | // figure out what the last round was 68 | const status = await nodeRequest(`/v2/status/`); 69 | const lastRound = status['last-round']; 70 | 71 | // figure out which keys are stored on this node 72 | let nodeKeys = await nodeRequest('/v2/participation'); 73 | if (!Array.isArray(nodeKeys)) { 74 | nodeKeys = []; 75 | } 76 | 77 | for (const nodeKey of nodeKeys) { 78 | // delete any key that is expired 79 | if (nodeKey.key['vote-last-valid'] < lastRound) { 80 | await window.goal.deletepartkey(nodeKey.id); 81 | continue; 82 | } 83 | 84 | // make sure we display the latest key 85 | if ( 86 | accounts[nodeKey.address] && 87 | (accounts[nodeKey.address].nodeParticipation.voteLast || 0) < 88 | nodeKey.key['vote-last-valid'] 89 | ) { 90 | accounts[nodeKey.address].nodeParticipation = { 91 | id: nodeKey.id, 92 | selectionKey: nodeKey.key['selection-participation-key'], 93 | stateProofKey: nodeKey.key['state-proof-key'], 94 | voteFirst: nodeKey.key['vote-first-valid'], 95 | voteKey: nodeKey.key['vote-participation-key'], 96 | voteKeyDilution: nodeKey.key['vote-key-dilution'], 97 | voteLast: nodeKey.key['vote-last-valid'], 98 | }; 99 | } 100 | } 101 | 102 | return (state) => 103 | produce(state, (draft) => { 104 | draft.accounts = accounts; 105 | }); 106 | }); 107 | 108 | store.register( 109 | 'accounts/add', 110 | async (_, address: string, shouldSave = true) => { 111 | try { 112 | const response = await nodeRequest(`/v2/accounts/${address}`); 113 | const chainKey = response.participation; 114 | 115 | let nodeKeys = await nodeRequest('/v2/participation'); 116 | if (!Array.isArray(nodeKeys)) { 117 | nodeKeys = []; 118 | } 119 | 120 | const nodeKey = nodeKeys 121 | .filter((k: any) => k.address === address) 122 | .reduce((nodeKey: any, currentKey: any) => { 123 | if ( 124 | !nodeKey || 125 | nodeKey.key['vote-last-valid'] < currentKey.key['vote-last-valid'] 126 | ) { 127 | return currentKey; 128 | } 129 | 130 | return nodeKey; 131 | }, null); 132 | 133 | return (state) => 134 | produce(state, (draft) => { 135 | draft.accounts[address] = { 136 | address, 137 | algoAmount: response.amount, 138 | chainParticipation: chainKey 139 | ? { 140 | selectionKey: chainKey['selection-participation-key'], 141 | stateProofKey: chainKey['state-proof-key'], 142 | voteFirst: chainKey['vote-first-valid'], 143 | voteKey: chainKey['vote-participation-key'], 144 | voteKeyDilution: chainKey['vote-key-dilution'], 145 | voteLast: chainKey['vote-last-valid'], 146 | } 147 | : {}, 148 | nodeParticipation: nodeKey 149 | ? { 150 | id: nodeKey.id, 151 | selectionKey: nodeKey.key['selection-participation-key'], 152 | stateProofKey: nodeKey.key['state-proof-key'], 153 | voteFirst: nodeKey.key['vote-first-valid'], 154 | voteKey: nodeKey.key['vote-participation-key'], 155 | voteKeyDilution: nodeKey.key['vote-key-dilution'], 156 | voteLast: nodeKey.key['vote-last-valid'], 157 | } 158 | : {}, 159 | pk: algosdk.decodeAddress(address).publicKey, 160 | stats: { 161 | lastProposedBlock: 162 | state.accounts[address]?.stats.lastProposedBlock || 0, 163 | proposals: state.accounts[address]?.stats.proposals || 0, 164 | votes: state.accounts[address]?.stats.votes || 0, 165 | }, 166 | }; 167 | 168 | if (shouldSave) { 169 | flux.dispatch('accounts/save'); 170 | } 171 | }); 172 | } catch (err) { 173 | flux.dispatch( 174 | 'notices/error', 175 | `Failed to fetch account: ${err.message}\n${err.toString()}`, 176 | ); 177 | } 178 | }, 179 | ); 180 | 181 | store.register('accounts/refresh', async (dispatch) => { 182 | let promises = []; 183 | for (const account of store.selectState('list')) { 184 | promises.push(dispatch('accounts/add', account, false)); 185 | } 186 | 187 | await Promise.all(promises); 188 | dispatch('accounts/save'); 189 | }); 190 | 191 | store.register( 192 | 'accounts/remove', 193 | (_, account: string) => (state) => 194 | produce(state, (draft) => { 195 | delete draft.accounts[account]; 196 | flux.dispatch('accounts/save'); 197 | }), 198 | ); 199 | 200 | store.register( 201 | 'accounts/reset-stats', 202 | (_, account: string) => (state) => 203 | produce(state, (draft) => { 204 | draft.accounts[account].stats.proposals = 0; 205 | draft.accounts[account].stats.votes = 0; 206 | flux.dispatch('accounts/save'); 207 | }), 208 | ); 209 | 210 | store.register( 211 | 'accounts/save', 212 | () => void window.store.set('accounts', store.selectState().accounts), 213 | ); 214 | 215 | store.register( 216 | 'accounts/stats/addProposal', 217 | (_, address: string, block: number) => (state) => 218 | produce(state, (draft) => { 219 | draft.accounts[address].stats.lastProposedBlock = block; 220 | draft.accounts[address].stats.proposals++; 221 | flux.dispatch('accounts/save'); 222 | }), 223 | ); 224 | 225 | let lastVoteSave = 0; 226 | store.register( 227 | 'accounts/stats/addVote', 228 | (_, address: string) => (state) => 229 | produce(state, (draft) => { 230 | draft.accounts[address].stats.votes++; 231 | let now = new Date().getTime(); 232 | if (now - lastVoteSave > VOTE_SAVE_INTERVAL) { 233 | lastVoteSave = now; 234 | flux.dispatch('accounts/save'); 235 | } 236 | }), 237 | ); 238 | 239 | store.addSelector('anyParticipating', (state) => 240 | Object.values(state.accounts).some( 241 | (account) => account.chainParticipation.voteKey !== undefined, 242 | ), 243 | ); 244 | 245 | store.addSelector('get', (state, address) => state.accounts[address]); 246 | 247 | store.addSelector('list', (state) => 248 | Object.values(state.accounts) 249 | .sort((a, b) => b.algoAmount - a.algoAmount) 250 | .map((a) => a.address), 251 | ); 252 | 253 | store.addSelector( 254 | 'participating', 255 | (state, address) => 256 | state.accounts[address]?.chainParticipation.voteKey !== undefined, 257 | ); 258 | 259 | store.addSelector('totalProposals', (state) => 260 | Object.values(state.accounts) 261 | .map((account) => account.stats.proposals) 262 | .reduce((total, proposals) => total + proposals, 0), 263 | ); 264 | 265 | store.addSelector('totalStake', (state) => 266 | Object.values(state.accounts) 267 | .filter((account) => store.selectState('participating', account.address)) 268 | .map((account) => account.algoAmount) 269 | .reduce((total, algoAmount) => total + algoAmount, 0), 270 | ); 271 | 272 | store.addSelector('totalVotes', (state) => 273 | Object.values(state.accounts) 274 | .map((account) => account.stats.votes) 275 | .reduce((total, votes) => total + votes, 0), 276 | ); 277 | -------------------------------------------------------------------------------- /src/render/flux/index.ts: -------------------------------------------------------------------------------- 1 | import flux from '@aust/react-flux'; 2 | 3 | import './accountsStore'; 4 | import './wizardStore'; 5 | 6 | window.electron.isDev().then((isDev) => { 7 | flux.setOption('displayLogs', isDev); 8 | }); 9 | 10 | (window as any).flux = flux; 11 | -------------------------------------------------------------------------------- /src/render/flux/wizardStore.ts: -------------------------------------------------------------------------------- 1 | import flux, { Store } from '@aust/react-flux'; 2 | import { addNode, nodeRequest, removeNode } from 'algoseas-libs/build/algo'; 3 | import { produce } from 'immer'; 4 | 5 | const CATCHUP_THRESHOLD = 20000; // catchup is triggered if node is this many blocks behind (2 catchup intervals gaurantees we can actually catch up) 6 | const NODE_REQUEST_TIMEOUT = 3000; // how long to wait for a node request to complete 7 | const SYNC_WATCH_DELAY = 1000; // how long during syncing to wait between checks 8 | 9 | enum CatchUpStatus { 10 | Unchecked, 11 | CatchingUp, 12 | Completed, 13 | Unneeded, 14 | } 15 | 16 | export enum Step { 17 | Settings, 18 | Check_Node_Running, 19 | Node_Starting, 20 | Check_Node_Synced, 21 | Node_Syncing, 22 | Dashboard, 23 | } 24 | 25 | export enum Status { 26 | Pending, 27 | Success, 28 | Failure, 29 | } 30 | 31 | type WizardStoreState = { 32 | buffers: { 33 | stderr: string[]; 34 | stdout: string[]; 35 | }; 36 | catchUpStatus: CatchUpStatus; 37 | currentStep: Step; 38 | dataDir: ''; 39 | guid: ''; 40 | network: 'algorand.mainnet' | 'voi.mainnet'; 41 | nodeName: ''; 42 | port: number; 43 | stepStatus: Record; 44 | 45 | // selectors 46 | infraHash: string; 47 | networks: { label: string; value: string }[]; 48 | running: boolean; 49 | }; 50 | 51 | declare global { 52 | interface Flux { 53 | wizard: Store; 54 | } 55 | } 56 | 57 | const store = flux.addStore('wizard', { 58 | buffers: { 59 | stderr: [], 60 | stdout: [], 61 | }, 62 | catchUpStatus: CatchUpStatus.Unchecked, 63 | currentStep: Step.Check_Node_Running, 64 | dataDir: '', 65 | network: 'algorand.mainnet', 66 | nodeName: '', 67 | port: 4160, 68 | stepStatus: Object.entries(Step).reduce((stepStatus, [, value]) => { 69 | if (typeof value === 'string') { 70 | return stepStatus; 71 | } 72 | 73 | if (value === Step.Check_Node_Running) { 74 | stepStatus[value] = Status.Pending; 75 | return stepStatus; 76 | } 77 | 78 | stepStatus[value] = Status.Failure; 79 | return stepStatus; 80 | }, {} as Record), 81 | }) as any as Store; 82 | 83 | store.register('wizard/loadConfig', async () => { 84 | const { dataDir, guid, network, port, store } = 85 | await window.electron.loadConfig(); 86 | window.store = store; 87 | 88 | return (state) => 89 | produce(state, (draft) => { 90 | draft.dataDir = dataDir; 91 | draft.guid = guid; 92 | draft.network = network as any; 93 | draft.port = port; 94 | }); 95 | }); 96 | 97 | store.register( 98 | 'wizard/overview/goto', 99 | (_, step) => (state) => 100 | produce(state, (draft) => { 101 | draft.buffers = { stderr: [], stdout: [] }; 102 | 103 | // if they are on the settings page, don't yank them away 104 | if (draft.currentStep !== Step.Settings) { 105 | draft.currentStep = step; 106 | } 107 | 108 | for (let _step in Step) { 109 | const index = Number(_step); 110 | if (isNaN(index)) { 111 | continue; 112 | } 113 | 114 | draft.stepStatus[index as Step] = 115 | index < step 116 | ? Status.Success 117 | : index === step 118 | ? Status.Pending 119 | : Status.Failure; 120 | } 121 | }), 122 | ); 123 | 124 | store.register('wizard/checkNodeRunning', () => { 125 | flux.dispatch('wizard/overview/goto', Step.Check_Node_Running); 126 | flux.dispatch('wizard/checkNodeRunning/results'); 127 | }); 128 | 129 | store.register('wizard/checkNodeRunning/results', async () => { 130 | const running = await window.goal.running(); 131 | if (!running) { 132 | flux.dispatch('wizard/startNode'); 133 | } else { 134 | flux.dispatch('wizard/checkNodeSynced'); 135 | } 136 | }); 137 | 138 | store.register('wizard/startNode', () => { 139 | flux.dispatch('wizard/overview/goto', Step.Node_Starting); 140 | flux.dispatch('wizard/startNode/results'); 141 | }); 142 | 143 | let nodeAdded = false; 144 | store.register('wizard/startNode/results', async () => { 145 | try { 146 | await window.goal.start(); 147 | const token = await window.goal.token(); 148 | addNode( 149 | `http://localhost:${store.selectState('port')}`, 150 | token, 151 | 'X-Algo-API-Token', 152 | ); 153 | nodeAdded = true; 154 | flux.dispatch('node/ready'); 155 | 156 | await waitForNodeProgress(); 157 | 158 | flux.dispatch('wizard/checkNodeSynced'); 159 | } catch (err) { 160 | return (state) => 161 | produce(state, (draft) => { 162 | draft.buffers.stderr = [err.toString()]; 163 | draft.stepStatus[Step.Node_Starting] = Status.Failure; 164 | }); 165 | } 166 | }); 167 | 168 | store.register('wizard/checkNodeSynced', () => { 169 | flux.dispatch('wizard/overview/goto', Step.Check_Node_Synced); 170 | flux.dispatch('wizard/checkNodeSynced/results'); 171 | }); 172 | 173 | store.register('wizard/checkNodeSynced/results', async () => { 174 | try { 175 | await checkNodeReady(); 176 | flux.dispatch('wizard/showDashboard'); 177 | } catch (err) { 178 | flux.dispatch('wizard/syncNode'); 179 | } 180 | }); 181 | 182 | store.register('wizard/syncNode', () => { 183 | flux.dispatch('wizard/overview/goto', Step.Node_Syncing); 184 | 185 | return (state) => 186 | produce(state, (draft) => { 187 | flux.dispatch('wizard/syncNode/results'); 188 | draft.catchUpStatus = CatchUpStatus.Unchecked; 189 | }); 190 | }); 191 | 192 | store.register('wizard/syncNode/results', async () => { 193 | try { 194 | let catchUpStatus = store.selectState('catchUpStatus'); 195 | if (catchUpStatus === CatchUpStatus.CatchingUp) { 196 | try { 197 | // after catching up, the node will report as ready 198 | // but the node still needs to download more blocks 199 | // so if the request is successful, we know that 200 | // we are back to syncing normally. so we wait a bit 201 | // and then check sync results again 202 | await checkNodeReady(); 203 | await waitForNodeProgress(); 204 | catchUpStatus = CatchUpStatus.Completed; 205 | } catch (err) { 206 | // still catching up 207 | } 208 | 209 | const output = await window.goal.status(); 210 | return (state) => 211 | produce(state, (draft) => { 212 | draft.buffers.stderr = [output]; 213 | draft.catchUpStatus = catchUpStatus; 214 | 215 | const hash = store.selectState('infraHash'); 216 | setTimeout(() => { 217 | // make sure that the user didn't change the network or port 218 | if (hash !== store.selectState('infraHash')) { 219 | return; 220 | } 221 | 222 | flux.dispatch('wizard/syncNode/results'); 223 | }, SYNC_WATCH_DELAY); 224 | }); 225 | } 226 | 227 | if (catchUpStatus === CatchUpStatus.Unchecked) { 228 | // get the catchpoint for the network 229 | const catchpoint = (await window.goal.catchpoint()).trim(); 230 | const catchpointRound = +catchpoint.split('#')[0]; 231 | 232 | // compare the node to the catchpoint 233 | const status = await nodeRequest('/v2/status'); 234 | const isCatchingUp = status['catchpoint'] === catchpoint; 235 | const needsCatchUp = 236 | catchpointRound - status['last-round'] > CATCHUP_THRESHOLD; 237 | 238 | // we check to see if it is catching up in case the user 239 | // restarts the app during catchup 240 | if (isCatchingUp) { 241 | catchUpStatus = CatchUpStatus.CatchingUp; 242 | } else if (needsCatchUp) { 243 | await window.goal.catchup(catchpoint); 244 | catchUpStatus = CatchUpStatus.CatchingUp; 245 | } else { 246 | catchUpStatus = CatchUpStatus.Unneeded; 247 | } 248 | } 249 | 250 | try { 251 | await checkNodeReady(); 252 | flux.dispatch('wizard/showDashboard'); 253 | } catch (err) { 254 | // still not synced 255 | const output = await window.goal.status(); 256 | return (state) => 257 | produce(state, (draft) => { 258 | draft.buffers.stderr = [output]; 259 | draft.catchUpStatus = catchUpStatus; 260 | 261 | const hash = store.selectState('infraHash'); 262 | setTimeout(() => { 263 | // make sure that the user didn't change the network or port 264 | if (hash !== store.selectState('infraHash')) { 265 | return; 266 | } 267 | 268 | flux.dispatch('wizard/syncNode/results'); 269 | }, SYNC_WATCH_DELAY); 270 | }); 271 | } 272 | } catch (err) { 273 | return (state) => 274 | produce(state, (draft) => { 275 | draft.buffers.stderr = [err.toString()]; 276 | draft.stepStatus[Step.Node_Syncing] = Status.Failure; 277 | }); 278 | } 279 | }); 280 | 281 | store.register('wizard/showDashboard', () => { 282 | flux.dispatch('wizard/overview/goto', Step.Dashboard); 283 | }); 284 | 285 | store.register( 286 | 'wizard/showSettings', 287 | () => (state) => 288 | produce(state, (draft) => { 289 | draft.currentStep = Step.Settings; 290 | }), 291 | ); 292 | 293 | store.register( 294 | 'wizard/return', 295 | () => (state) => 296 | produce(state, (draft) => { 297 | // see if we need to return to the dashboard 298 | if (draft.stepStatus[Step.Dashboard] !== Status.Failure) { 299 | draft.currentStep = Step.Dashboard; 300 | return; 301 | } 302 | 303 | // return to the current pending step that's not settings 304 | for (let step in draft.stepStatus) { 305 | let index = Number(step); 306 | if (isNaN(index)) { 307 | continue; 308 | } 309 | 310 | if ( 311 | index !== Step.Settings && 312 | draft.stepStatus[index as Step] === Status.Pending 313 | ) { 314 | draft.currentStep = index; 315 | return; 316 | } 317 | } 318 | }), 319 | ); 320 | 321 | store.register('wizard/setDataDir', async (_, dataDir) => { 322 | if (nodeAdded) { 323 | removeNode(`http://localhost:${store.selectState('port')}`); 324 | nodeAdded = false; 325 | } 326 | 327 | await window.store.set('dataDir', dataDir); 328 | 329 | return (state) => 330 | produce(state, (draft) => { 331 | draft.dataDir = dataDir; 332 | }); 333 | }); 334 | 335 | store.register('wizard/setNetwork', async (_, network) => { 336 | if (nodeAdded) { 337 | removeNode(`http://localhost:${store.selectState('port')}`); 338 | nodeAdded = false; 339 | } 340 | 341 | await window.electron.swapNetwork(network); 342 | await flux.dispatch('wizard/loadConfig'); 343 | }); 344 | 345 | store.register('wizard/setPort', async (_, port) => { 346 | if (nodeAdded) { 347 | removeNode(`http://localhost:${store.selectState('port')}`); 348 | nodeAdded = false; 349 | } 350 | 351 | await window.store.set('port', port); 352 | 353 | return (state) => 354 | produce(state, (draft) => { 355 | draft.port = port; 356 | }); 357 | }); 358 | 359 | store.register('wizard/setTelemetry', async (_, nodeName) => { 360 | const guid = await window.goal.telemetry( 361 | nodeName, 362 | store.selectState('network'), 363 | ); 364 | 365 | await window.store.set('guid', guid); 366 | await window.store.set('nodeName', nodeName); 367 | 368 | return (state) => 369 | produce(state, (draft) => { 370 | draft.nodeName = nodeName; 371 | draft.guid = guid; 372 | }); 373 | }); 374 | 375 | store.register('wizard/stopNode', async () => { 376 | await window.goal.stop(); 377 | flux.dispatch('wizard/overview/goto', Step.Settings); 378 | }); 379 | 380 | store.register( 381 | 'wizard/stderr', 382 | (_, data) => (state) => 383 | produce(state, (draft) => void draft.buffers.stderr.push(data)), 384 | ); 385 | 386 | store.register( 387 | 'wizard/stdout', 388 | (_, data) => (state) => 389 | produce(state, (draft) => void draft.buffers.stdout.push(data)), 390 | ); 391 | 392 | // changing the network, port, telemetry, or dataDir requires a node restart 393 | // infraHash is a shortcut to check for that 394 | store.addSelector( 395 | 'infraHash', 396 | (state) => state.network + state.nodeName + state.port + state.dataDir, 397 | ); 398 | 399 | store.addSelector('networks', () => [ 400 | { label: 'Algorand MainNet', value: 'algorand.mainnet' }, 401 | { label: 'Voi MainNet', value: 'voi.mainnet' }, 402 | ]); 403 | 404 | store.addSelector( 405 | 'running', 406 | (state) => state.stepStatus[Step.Dashboard] !== Status.Failure, 407 | ); 408 | 409 | async function checkNodeReady() { 410 | await nodeRequest('/ready', { 411 | fetchTimeoutMs: NODE_REQUEST_TIMEOUT, 412 | maxRetries: 0, 413 | }); 414 | } 415 | 416 | async function waitForNodeProgress() { 417 | let startBlock: number | null = null; 418 | while (true) { 419 | try { 420 | const status = await nodeRequest('/v2/status/', { 421 | fetchTimeoutMs: NODE_REQUEST_TIMEOUT, 422 | maxRetries: 0, 423 | }); 424 | 425 | if (startBlock === null) { 426 | startBlock = status['last-round']; 427 | throw new Error('Node is not ready. Setting start block.'); 428 | } 429 | 430 | if (status['last-round'] === startBlock && status['catchpoint'] === '') { 431 | throw new Error( 432 | 'Node is not ready. Block round is the same and not catching up.', 433 | ); 434 | } 435 | 436 | break; 437 | } catch (err) { 438 | await new Promise((resolve) => setTimeout(resolve, SYNC_WATCH_DELAY)); 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/render/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #root { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font-family: "Montserrat"; 13 | } 14 | 15 | select { 16 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); 17 | background-position: right 0.5rem center; 18 | background-repeat: no-repeat; 19 | background-size: 1.5em 1.5em; 20 | padding-right: 2.5rem; 21 | -webkit-print-color-adjust: exact; 22 | print-color-adjust: exact; 23 | } -------------------------------------------------------------------------------- /src/render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aust's One-Click Node 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/render/utils.ts: -------------------------------------------------------------------------------- 1 | export function abbreviateNumber(number: number): string { 2 | return Intl.NumberFormat(undefined, { 3 | maximumSignificantDigits: 3, 4 | minimumSignificantDigits: number < 10 ? 1 : 2, 5 | notation: 'compact', 6 | }).format(number); 7 | } 8 | 9 | export function formatNumber(number: number): string { 10 | try { 11 | return Intl.NumberFormat(undefined, { 12 | maximumSignificantDigits: Math.max(3, Math.ceil(Math.log10(number))), 13 | minimumSignificantDigits: number < 10 ? 1 : 2, 14 | }).format(number); 15 | } catch (e) { 16 | if (Number.isNaN(number)) { 17 | return ''; 18 | } 19 | 20 | return number.toString(); 21 | } 22 | } 23 | 24 | // modified from https://stackoverflow.com/a/45309230/1408717 25 | export function parseNumber(value: string, defaultValue: number = 0) { 26 | if (!value) { 27 | return defaultValue; 28 | } 29 | 30 | const decimal = Intl.NumberFormat() 31 | .formatToParts(1.1) 32 | .find((part) => part.type === 'decimal')!.value; 33 | 34 | return parseFloat( 35 | value 36 | .replace(new RegExp(`[^-+0-9${decimal}]`, 'g'), '') 37 | .replace(decimal, '.'), 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file will automatically be loaded by webpack and run in the "renderer" context. 3 | * To learn more about the differences between the "main" and the "renderer" context in 4 | * Electron, visit: 5 | * 6 | * https://electronjs.org/docs/latest/tutorial/process-model 7 | * 8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration 9 | * in a renderer process, please be aware of potential security implications. You can read 10 | * more about security risks here: 11 | * 12 | * https://electronjs.org/docs/tutorial/security 13 | * 14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` 15 | * flag: 16 | * 17 | * ``` 18 | * // Create the browser window. 19 | * mainWindow = new BrowserWindow({ 20 | * width: 800, 21 | * height: 600, 22 | * webPreferences: { 23 | * nodeIntegration: true 24 | * } 25 | * }); 26 | * ``` 27 | */ 28 | 29 | import ReactDOM from 'react-dom'; 30 | 31 | import App from './render/App'; 32 | 33 | ReactDOM.render(App(), document.getElementById('root')); 34 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,ts,tsx}'], 4 | darkMode: 'class', 5 | plugins: [], 6 | theme: { 7 | extend: {}, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "jsx": "react-jsx", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "outDir": "dist", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "paths": { 17 | "*": ["node_modules/*"], 18 | "@/*": ["src/*"], 19 | "@assets/*": ["assets/*"], 20 | "@components/*": ["src/render/components/*"], 21 | } 22 | }, 23 | "include": ["assets/**/*", "src/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /webpack.main.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'webpack'; 2 | 3 | import { rules } from './webpack.rules'; 4 | 5 | export const mainConfig: Configuration = { 6 | devtool: 'inline-source-map', 7 | /** 8 | * This is the main entry point for your application, it's the first file 9 | * that runs in the main process. 10 | */ 11 | entry: './src/main.ts', 12 | // Put your normal webpack config below here 13 | module: { 14 | rules, 15 | }, 16 | plugins: [], 17 | resolve: { 18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /webpack.plugins.ts: -------------------------------------------------------------------------------- 1 | import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 2 | import NodePolyfillPlugin from 'node-polyfill-webpack-plugin'; 3 | import { NormalModuleReplacementPlugin } from 'webpack'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 7 | 8 | export const plugins = [ 9 | new ForkTsCheckerWebpackPlugin({ 10 | logger: 'webpack-infrastructure', 11 | }), 12 | new NodePolyfillPlugin(), 13 | new NormalModuleReplacementPlugin(/^ws$/, 'isomorphic-ws'), 14 | // force webpack to use the browser versions of these modules (they are all loaded by crypto-browserify) 15 | new NormalModuleReplacementPlugin( 16 | /^browserify-cipher$/, 17 | 'browserify-cipher/browser', 18 | ), 19 | new NormalModuleReplacementPlugin(/^create-hmac$/, 'create-hmac/browser'), 20 | new NormalModuleReplacementPlugin(/^randombytes$/, 'randombytes/browser'), 21 | ]; 22 | -------------------------------------------------------------------------------- /webpack.renderer.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TSConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; 3 | import type { Configuration } from 'webpack'; 4 | 5 | import { rules } from './webpack.rules'; 6 | import { plugins } from './webpack.plugins'; 7 | 8 | rules.push({ 9 | test: /\.css$/, 10 | use: [ 11 | { loader: 'style-loader' }, 12 | { loader: 'css-loader', options: { importLoaders: 1 } }, 13 | { 14 | loader: 'postcss-loader', 15 | options: { 16 | postcssOptions: { 17 | config: path.join(__dirname, 'postcss.config.js'), 18 | }, 19 | }, 20 | }, 21 | , 22 | ], 23 | }); 24 | 25 | export const rendererConfig: Configuration = { 26 | devtool: 'inline-source-map', 27 | module: { 28 | rules, 29 | }, 30 | plugins, 31 | resolve: { 32 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], 33 | plugins: [new TSConfigPathsPlugin({ baseUrl: '.' })], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /webpack.rules.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleOptions } from 'webpack'; 2 | 3 | export const rules: Required['rules'] = [ 4 | // Add support for native node modules 5 | { 6 | // We're specifying native_modules in the test because the asset relocator loader generates a 7 | // "fake" .node file which is really a cjs file. 8 | test: /native_modules[/\\].+\.node$/, 9 | use: 'node-loader', 10 | }, 11 | // { 12 | // test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, 13 | // parser: { amd: false }, 14 | // use: { 15 | // loader: '@vercel/webpack-asset-relocator-loader', 16 | // options: { 17 | // outputAssetBase: 'native_modules', 18 | // }, 19 | // }, 20 | // }, 21 | { 22 | test: /\.tsx?$/, 23 | exclude: /(node_modules|\.webpack)/, 24 | use: { 25 | loader: 'ts-loader', 26 | options: { 27 | transpileOnly: true, 28 | }, 29 | }, 30 | }, 31 | { 32 | test: /\.jsx?$/, 33 | exclude: /node_modules/, 34 | use: { 35 | loader: 'babel-loader', 36 | options: { 37 | presets: [ 38 | [ 39 | '@babel/preset-react', 40 | { 41 | runtime: 'automatic', 42 | }, 43 | ], 44 | ], 45 | }, 46 | }, 47 | }, 48 | { 49 | test: /\.(png|svg|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/i, 50 | type: 'asset', 51 | generator: { 52 | filename: 'assets/[hash][ext]', 53 | }, 54 | parser: { 55 | dataUrlCondition: { 56 | maxSize: 4 * 1024, 57 | }, 58 | }, 59 | }, 60 | ]; 61 | --------------------------------------------------------------------------------