diff --git a/.github/workflows/release_win.yml b/.github/workflows/release_win.yml index de00e3d1..55bba7b5 100644 --- a/.github/workflows/release_win.yml +++ b/.github/workflows/release_win.yml @@ -8,6 +8,13 @@ on: jobs: build-windows: runs-on: windows-latest + strategy: + matrix: + include: + - env: "production" + rpc: "https://rpc-gate.autonolas.tech/gnosis-rpc/" + - env: "development" + rpc: "https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b" defaults: run: shell: bash @@ -33,15 +40,15 @@ jobs: - name: Install dependencies run: poetry install - - name: install node deps + - name: install all deps run: yarn install-deps - name: set env vars to prod.env env: - NODE_ENV: production - DEV_RPC: https://rpc-gate.autonolas.tech/gnosis-rpc/ + NODE_ENV: ${{ matrix.env }} + DEV_RPC: ${{ matrix.rpc }} IS_STAGING: ${{ github.ref != 'refs/heads/main' && 'true' || 'false' }} - FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/ + FORK_URL: ${{ matrix.rpc }} GH_TOKEN: ${{ secrets.github_token}} run: | echo NODE_ENV=$NODE_ENV >> prod.env diff --git a/.gitleaks.toml b/.gitleaks.toml index 7dbdf15e..975e353c 100755 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -534,8 +534,12 @@ regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\| entropy = 3.7 secretGroup = 4 - [allowlist] -description = "global allow lists" -regexes = ['''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}'''] -paths = ['''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$'''] +description = "allowlist" +regexTarget = "match" +regexes = [ + '''\b(0x)?[0-9a-fA-F]{40}\b''', # ignore evm public keys + '''219-09-9999''', # global allow lists + '''078-05-1120''', + '''(9[0-9]{2}|666)-\d{2}-\d{4}''' +] \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 922f10a1..2edeafb0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.x +20 \ No newline at end of file diff --git a/README.md b/README.md index 76b4ede7..ebb5de65 100644 --- a/README.md +++ b/README.md @@ -2,223 +2,47 @@ Pearl -Pearl is an application used to run autonomous agents powered by the OLAS Network. - -## Technologies Used - -- Electron -- NodeJS (20.11 LTS) -- AntD (^5) -- NextJS (^14) -- Javascript / TypeScript -- Python (3.10) -- Poetry (^1.7.1) +A cross-platform desktop application used to run autonomous agents powered by the OLAS Network. ## Getting Started -### Installing system dependencies - -The following installation steps assume you have the following on each OS: - -- Linux: a debian based operating system such as Ubuntu with `apt` to install packages. -- MacOS: [Homebrew](https://brew.sh/) - -

NodeJS

- -NodeJS is best installed and managed through NVM. It allows you to install and select specific versions of NodeJS. Pearl has been built using version 20 LTS. - -
Linux
- -```bash -sudo apt install curl -curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash -source ~/.bashrc -nvm install --lts -nvm use --lts -``` - -
MacOS
- -```bash -brew install nvm -``` - -Set up NVM for console usage. Dependant on the shell, you should edit the config file to contain the following code. -If you're using Bash or Zsh, you might add them to your `~/.bash_profile`, `~/.bashrc`, or `~/.zshrc` file: - -```bash -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion -``` - -Close and reopen Terminal, or run `source ~/.bash_profile`, `source ~/.zshrc`, or `source ~/.bashrc` to reload the shell configuration. - -Verify your installation by running `nvm --version`. Then run: - -```bash -nvm install --lts -nvm use --lts -``` - -
- -

Yarn

- -Yarn is the package manager used for dependency management of the Electron app and NextJS frontend. - -```bash -npm install --global yarn -``` -
- -

Python

- -
Linux
- -```bash -sudo apt install python3 -``` - -
MacOS
- -```bash -brew install python -``` - -
- -

PIPX

- -
Linux
- -```bash -sudo apt install pipx -``` - -
MacOS
- -```bash -brew install pipx -``` - -
- -

Poetry

- -Poetry is used on the backend to install and manage dependencies, and create a virtual environment for the backend API. - -```bash -pipx install poetry -``` - -If promoted to run `pipx ensurepath`, run it. - -
- -

Setting up your .env file

- -Create an `.env` file in the root directory, or rename `.env.example` to `.env`. -Then set the following environment variables. - -

NODE_ENV

- -For development usage, set `NODE_ENV=development`. -For production usage, set `NODE_ENV=production`. - -
- -

FORK_URL

- -**This variable is required for both development and production.** -**Must be a Gnosis Mainnet RPC URL.** - -- In `development` this RPC url is only used if/when forking mainnet with Hardhat (covered later). This process allows you to test without losing funds. -- In `production` this RPC URL is used as the main RPC for Pearl. - -You can get a Gnosis RPC from [Nodies](https://www.nodies.app/). - -Once you have a Gnosis Mainnet RPC URL, set `FORK_URL=YOUR_RPC_URL_HERE` in your .env file. - -Note: this must be an external RPC. If you decide to use Hardhat for testing on a mainnet fork, do _not_ set your Hardhat Node URL here. -
- -

DEV_RPC

- -This environment variable is only used when `NODE_ENV=development` is set. - -In `development` mode, it is used throughout Pearl as the main RPC. - -If you're using Hardhat, you can set `DEV_RPC=http://localhost:8545`. -Or, you can use another, external RPC URL that wish to test on, ensuring that the chain ID is 100 (Gnosis Mainnet's chain ID). - -
- -

Installing project dependencies

- -Run the following command to install all project dependencies. - -```bash -yarn install-deps -``` - -

Running the application

- -Provided your system dependencies are installed, environment variables are set, and your RPC is running. - -You can start Pearl by running the following command in the root directory: - -```bash -yarn dev -``` - -This will run Electron, which launches the NextJS frontend and the Python backend as child processes. - -

Chain forking (for development)

- -In the interest of protecting your funds during development, you can run a forked version of Gnosis Mainnet. - -There are two recommended options, choose one: - -

Tenderly (preferred)

+### For Users -[Tenderly](https://tenderly.co/) is a service with a plethora of useful blockchain development tools. The tool required here gives you the ability to **fork networks**. +#### Downloading the latest release -You can also monitor all transactions, and fund your accounts with any token that you please. +**Note:** The release pages also contain Source Code `.zip` files and `dev-` prefixed builds. These are not intended for general use. Ignore them unless you're a developer! -1. Signup to [Tenderly](https://tenderly.co/), and select the plan you desire. **The Free plan should suffice for most users**. -2. Go to *Forks* under the *Development* tab -- in the left sidebar of your dashboard. -3. Click *Create Fork*, select "Gnosis Chain" as the network, and use Chain ID `100`. -4. Copy the RPC url into the appropriate .env variables in your repository. (Recommended to set both `FORK_URL` & `DEV_RPC` to this RPC url during development). -5. Click the *Fund Accounts* button to fund your accounts with XDAI (native token) and [OLAS](https://gnosisscan.io/token/0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f). +- Go to the [Releases](https://github.com/valory-xyz/olas-operate-app/releases) page. +- Download the latest release for your operating system. + - If you're on Windows, download the `.exe` file. + - If you're on MacOS, download the `.dmg` file. + - Both Intel x64 and Apple Silicon ARM64 builds are available. +- Install the application by running the downloaded file. -
+### For Developers -

Hardhat

-Note: using Hardhat will result in the loss of chain state once your Hardhat node is turned off. +#### Setting up your development environment -Run the following command in the root of your project folder to start your Hardhat node: +- [Ubuntu Setup Guide](docs/dev/ubuntu-setup.md) +- [MacOS Setup Guide](docs/dev/macos-setup.md) +- [Windows Setup Guide](docs/dev/windows-setup.md) -```bash -npx hardhat node -``` +#### Setting up a development RPC endpoint -**Once Hardhat is running, you will be able to use `http://localhost:8545` as your development RPC.** +- [RPC Setup Guide](docs/dev/rpcs.md) -
Funding your addresses with Hardhat
+## Project Dependencies -There are scripts to fund addresses during testing/development: +There are three parts to the project: the Electron app (CommonJS), the NextJS frontend (TypeScript), and the Python backend/middleware. -- XDAI funding: +- [Electron dependencies](package.json) +- [Frontend dependencies](package.json) +- [Backend dependencies](backend/pyproject.toml) -```bash -poetry run python scripts/fund.py 0xYOURADDRESS -``` +## License -- OLAS funding: +- [Apache 2.0](LICENSE) -```bash -poetry run python scripts/transfer_olas.py PATH_TO_KEY_CONTAINING_OLAS ADDRESS_TO_TRANSFER AMOUNT -``` +## Security -
\ No newline at end of file +- [Security Policy](SECURITY.md) diff --git a/docs/dev/macos-setup.md b/docs/dev/macos-setup.md new file mode 100644 index 00000000..0193e72e --- /dev/null +++ b/docs/dev/macos-setup.md @@ -0,0 +1,97 @@ +# Setting up Pearl for development on MacOS + +### System dependencies + +## 1. Brew + +Brew is a package manager for MacOS, allowing you to install packages from the command line. + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +## 2. Node Version Manager (NVM) + +NVM is a version manager for Node.js, allowing you to switch between different versions of Node.js. + +```bash +brew install nvm +``` + +Set up NVM for console usage. Dependant on the shell, you should edit the config file to contain the following code. + +If you're using Bash or Zsh, you might add them to your `~/.bash_profile`, `~/.bashrc`, or `~/.zshrc` file: + +```bash +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion +``` + +Close and reopen Terminal, or run `source ~/.bash_profile`, `source ~/.zshrc`, or `source ~/.bashrc` to reload the shell configuration. + +## 3. Node.js + +```bash +nvm install +nvm use +``` + +## 4. Yarn + +Yarn is the package manager used for dependency management of the Electron app and NextJS frontend. + +```bash +npm install --global yarn +``` + +## 5. Python + +Use Python 3.10 for the project. + +```bash +brew install python@3.10 +``` + +## 6. Pipx + +```bash +brew install pipx +``` + +## 7. Poetry + +```bash +pipx install poetry +``` + +If prompted to add the `poetry` command to your shell's config file, accept the prompt. + +### Installing project dependencies + +The `install-deps` script will install the dependencies for all parts of the project. +The Electron app, the NextJS frontend, and the Python backend. + +```bash +yarn install-deps +``` + +### Setup the .env file + +Duplicate the `.env.example` file and rename it to `.env`. + +```bash +cp .env.example .env +``` + +Then fill in the required environment variables. + +- `NODE_ENV` - Set to `development` for development. `production` is only used for production builds built through the release script. +- `FORK_URL` - Set to your desired HTTP RPC endpoint. +- `DEV_RPC` - Set to the same value as `FORK_URL`. + +### Run the project + +```bash +yarn dev +``` diff --git a/docs/dev/rpcs.md b/docs/dev/rpcs.md new file mode 100644 index 00000000..bbd4502d --- /dev/null +++ b/docs/dev/rpcs.md @@ -0,0 +1,43 @@ +# Acquiring RPC Endpoints for Development + +## Tenderly + +We use Tenderly to fork the Gnosis Mainnet chain for development purposes. This allows us to interact with the chain without risking real funds. + +### 1. Create a Tenderly Account + +Go to [Tenderly](https://tenderly.co/) and create an account. + +### 2. Create a Project + +Create a new project in Tenderly. + +### 3. Fork the Gnosis Mainnet + +1. Go to the _Forks_ section under the _Development_ tab in your Tenderly dashboard. + +2. Click _Create Fork_. + +3. Select "Gnosis Chain" as the network. + +4. Use Chain ID `100`. + +5. Copy the RPC URL provided by Tenderly. + +### 4. Set the RPC URL + +Set the `FORK_URL` and `DEV_RPC` environment variables in your `.env` file to the RPC URL provided by Tenderly. + +### 5. Fund Your Accounts + +Click the _Fund Accounts_ button in Tenderly to fund your accounts with XDAI (native token) and [OLAS](https://gnosisscan.io/token/0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f). + +### 6. Keeping Your Fork Up-to-Date + +It is important to update your fork periodically to ensure that your forked chain is up-to-date with mainnet. You can do this by creating a new fork in Tenderly and updating your `FORK_URL` and `DEV_RPC` environment variables. + +Alternatively, you can try the Tenderly's virtual testnet feature, which can automatically update your fork for you relative to mainnet. Though, this sometimes results in instability. + +## Hardhat (deprecated) + +Hardhat is a local alternative to Tenderly for forking EVM chains. It is useful for development purposes, though the chain state is lost once the Hardhat node is turned off. diff --git a/docs/dev/ubuntu-setup.md b/docs/dev/ubuntu-setup.md new file mode 100644 index 00000000..47875b68 --- /dev/null +++ b/docs/dev/ubuntu-setup.md @@ -0,0 +1,79 @@ +# Setting up Pearl for development on Ubuntu + +### System dependencies + +## 1. Node Version Manager (NVM) + +NVM is a version manager for Node.js, allowing you to switch between different versions of Node.js. + +```bash +sudo apt install curl +curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash +source ~/.bashrc +``` + +## 3. Node.js + +```bash +nvm install +nvm use +``` + +## 4. Yarn + +Yarn is the package manager used for dependency management of the Electron app and NextJS frontend. + +```bash +npm install --global yarn +``` + +## 5. Python + +Use Python 3.10 for the project. + +```bash +sudo apt install python3.10 +``` + +## 6. Pipx + +```bash +sudo apt install pipx +``` + +## 7. Poetry + +```bash +pipx install poetry +``` + +If prompted to add the `poetry` command to your shell's config file, accept the prompt. + +### Installing project dependencies + +The `install-deps` script will install the dependencies for all parts of the project. +The Electron app, the NextJS frontend, and the Python backend. + +```bash +yarn install-deps +``` + +### Setup the .env file + +Duplicate the `.env.example` file and rename it to `.env`. + +```bash +cp .env.example .env +``` + +Then fill in the required environment variables. + +- `NODE_ENV` - Set to `development` for development. `production` is only used for production builds built through the release script. +- `FORK_URL` - Set to your desired HTTP RPC endpoint. +- `DEV_RPC` - Set to the same value as `FORK_URL`. + +### Run the project + +```bash +yarn dev +``` diff --git a/docs/dev/windows-setup.md b/docs/dev/windows-setup.md new file mode 100644 index 00000000..c56a1972 --- /dev/null +++ b/docs/dev/windows-setup.md @@ -0,0 +1,98 @@ +# Setting up Pearl for development on Windows + +- Development on Windows is experimental, but included here for reference. +- Please report any issues you encounter while setting up the project on Windows. +- You must be able to run PowerShell as an administrator to install the system dependencies. + +### Installing system dependencies + +## 1. Chocolatey + +Chocolatey is a package manager for Windows, allowing you to install packages from the command line. + +```powershell +# run as administrator +# https://chocolatey.org/install + +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +``` + +## 2. Node Version Manager (NVM) + +NVM is a version manager for Node.js, allowing you to switch between different versions of Node.js. + +```powershell +# run as administrator +choco install nvm +``` + +## 3. Node.js v20 + +```powershell +# run as administrator +nvm install 20 +nvm use 20 +``` + +## 4. Yarn + +Yarn is the package manager used for dependency management of the Electron app and NextJS frontend. + +```powershell +npm install --global yarn +``` + +## 5. Python 3.10 + +```powershell +# run as administrator +choco install python3.10 +``` + +## 6. Pipx + +```powershell +# run as administrator +python3.10 -m pip install pipx +``` + +## 7. Poetry + +```powershell +# run as administrator +pipx install poetry +``` + +If prompted to add `poetry` to your PATH, follow the prompt. + +### Installing project dependencies + +The `install-deps` script will install the dependencies for all parts of the project. +The Electron app, the NextJS frontend, and the Python backend. + +```powershell +# run from the project root +poetry shell +yarn install-deps +``` + +### Setup the .env file + +Duplicate the `.env.example` file and rename it to `.env`. + +```powershell +# run from the project root +cp .env.example .env +``` + +Then fill in the required environment variables. + +- `NODE_ENV` - Set to `development` for development. `production` is only used for production builds built through the release script. +- `FORK_URL` - Set to your desired HTTP RPC endpoint. +- `DEV_RPC` - Set to the _same_ value as `FORK_URL`. + +### Run the project + +```powershell +yarn dev +``` diff --git a/electron/assets/icons/tray-logged-out.png b/electron/assets/icons/tray-logged-out.png deleted file mode 100644 index 16311b50..00000000 Binary files a/electron/assets/icons/tray-logged-out.png and /dev/null differ diff --git a/electron/assets/icons/tray-low-gas.png b/electron/assets/icons/tray-low-gas.png deleted file mode 100644 index 189b5db8..00000000 Binary files a/electron/assets/icons/tray-low-gas.png and /dev/null differ diff --git a/electron/assets/icons/tray-paused.png b/electron/assets/icons/tray-paused.png deleted file mode 100644 index 86a3b150..00000000 Binary files a/electron/assets/icons/tray-paused.png and /dev/null differ diff --git a/electron/assets/icons/tray-running.png b/electron/assets/icons/tray-running.png deleted file mode 100644 index ba2387c7..00000000 Binary files a/electron/assets/icons/tray-running.png and /dev/null differ diff --git a/electron/assets/icons/tray/logged-out.png b/electron/assets/icons/tray/logged-out.png new file mode 100644 index 00000000..6f26a5e1 Binary files /dev/null and b/electron/assets/icons/tray/logged-out.png differ diff --git a/electron/assets/icons/tray/logged-out@2x.png b/electron/assets/icons/tray/logged-out@2x.png new file mode 100644 index 00000000..8748464f Binary files /dev/null and b/electron/assets/icons/tray/logged-out@2x.png differ diff --git a/electron/assets/icons/tray/logged-out@3x.png b/electron/assets/icons/tray/logged-out@3x.png new file mode 100644 index 00000000..b6b2ec8f Binary files /dev/null and b/electron/assets/icons/tray/logged-out@3x.png differ diff --git a/electron/assets/icons/tray/low-gas.png b/electron/assets/icons/tray/low-gas.png new file mode 100644 index 00000000..856cfbb9 Binary files /dev/null and b/electron/assets/icons/tray/low-gas.png differ diff --git a/electron/assets/icons/tray/low-gas@2x.png b/electron/assets/icons/tray/low-gas@2x.png new file mode 100644 index 00000000..8de86da7 Binary files /dev/null and b/electron/assets/icons/tray/low-gas@2x.png differ diff --git a/electron/assets/icons/tray/low-gas@3x.png b/electron/assets/icons/tray/low-gas@3x.png new file mode 100644 index 00000000..23e9a229 Binary files /dev/null and b/electron/assets/icons/tray/low-gas@3x.png differ diff --git a/electron/assets/icons/tray/paused.png b/electron/assets/icons/tray/paused.png new file mode 100644 index 00000000..4d13d7b7 Binary files /dev/null and b/electron/assets/icons/tray/paused.png differ diff --git a/electron/assets/icons/tray/paused@2x.png b/electron/assets/icons/tray/paused@2x.png new file mode 100644 index 00000000..eab10400 Binary files /dev/null and b/electron/assets/icons/tray/paused@2x.png differ diff --git a/electron/assets/icons/tray/paused@3x.png b/electron/assets/icons/tray/paused@3x.png new file mode 100644 index 00000000..2e398321 Binary files /dev/null and b/electron/assets/icons/tray/paused@3x.png differ diff --git a/electron/assets/icons/tray/running.png b/electron/assets/icons/tray/running.png new file mode 100644 index 00000000..858f061d Binary files /dev/null and b/electron/assets/icons/tray/running.png differ diff --git a/electron/assets/icons/tray/running@2x.png b/electron/assets/icons/tray/running@2x.png new file mode 100644 index 00000000..635ac5e0 Binary files /dev/null and b/electron/assets/icons/tray/running@2x.png differ diff --git a/electron/assets/icons/tray/running@3x.png b/electron/assets/icons/tray/running@3x.png new file mode 100644 index 00000000..e502fce2 Binary files /dev/null and b/electron/assets/icons/tray/running@3x.png differ diff --git a/electron/components/PearlTray.js b/electron/components/PearlTray.js new file mode 100644 index 00000000..506bea30 --- /dev/null +++ b/electron/components/PearlTray.js @@ -0,0 +1,147 @@ +const Electron = require('electron'); +const { isMac, isLinux, isWindows, isDev } = require('../constants'); +const { logger } = require('../logger'); + +// Used to resize the tray icon on macOS +const macTrayIconSize = { width: 16, height: 16 }; + +/** Status supported by tray icons. + * @readonly + * @enum {'logged-out' | 'low-gas' | 'paused' | 'running'} + */ +const TrayIconStatus = { + LoggedOut: 'logged-out', + LowGas: 'low-gas', + Paused: 'paused', + Running: 'running', +}; + +const appPath = Electron.app.getAppPath(); + +/** Paths to tray icons for different statuses. + * @readonly + * @type {Record} + */ +const trayIconPaths = { + [TrayIconStatus.LoggedOut]: `${appPath}/electron/assets/icons/tray/logged-out.png`, + [TrayIconStatus.LowGas]: `${appPath}/electron/assets/icons/tray/low-gas.png`, + [TrayIconStatus.Paused]: `${appPath}/electron/assets/icons/tray/paused.png`, + [TrayIconStatus.Running]: `${appPath}/electron/assets/icons/tray/running.png`, +}; + +/** Tray icons as native images + * @note macOS icons are resized + * @readonly + * @type {Record} */ +const trayIcons = Object.entries(trayIconPaths).reduce( + (acc, [status, path]) => ({ + ...acc, + [status]: (() => { + // Linux does not support nativeImage + if (isLinux) return path; + + // Windows and macOS support nativeImage + let trayIcon = Electron.nativeImage.createFromPath(path); + + if (isMac) { + // Resize icon for tray + trayIcon = trayIcon.resize(macTrayIconSize); + // Mark the image as a template image for MacOS to apply correct color + trayIcon.setTemplateImage(true); + } + + return trayIcon; + })(), + }), + {}, +); + +/** Cross-platform Electron Tray for Pearl, with context menu, icon, events. */ +class PearlTray extends Electron.Tray { + /** @param {() => Electron.BrowserWindow | null} activeWindowCallback */ + constructor(activeWindowCallback) { + // Set the tray icon to the logged-out state by default + super(trayIcons[TrayIconStatus.LoggedOut]); + + // Store the callback to retrieve the active window + this.activeWindowCallback = activeWindowCallback; + + this.setContextMenu(new PearlTrayContextMenu(activeWindowCallback)); + this.setToolTip('Pearl'); + + this.#bindClickEvents(); + this.#bindIpcListener(); + } + + #bindClickEvents = () => { + if (isWindows) { + isDev && logger.electron('binding windows click events to tray'); + // Windows: Handle single and double-clicks to show the window + this.on('click', () => this.activeWindowCallback()?.show()); + this.on('double-click', () => this.activeWindowCallback()?.show()); + this.on('right-click', () => this.popUpContextMenu()); + return; + } + isDev && + logger.electron('no click events bound to tray as not using win32'); + // macOS and Linux handle all clicks by displaying the context menu + // can show window by selecting 'Show app' on dropdown + // or clicking the app icon in the dock + }; + + #bindIpcListener = () => { + isDev && logger.electron('binding ipc listener for tray icon status'); + Electron.ipcMain.on('tray', (_event, status) => { + isDev && logger.electron('received tray icon status:', status); + switch (status) { + case TrayIconStatus.LoggedOut: { + this.setImage(trayIcons[TrayIconStatus.LoggedOut]); + break; + } + case TrayIconStatus.Running: { + this.setImage(trayIcons[TrayIconStatus.Running]); + break; + } + case TrayIconStatus.Paused: { + this.setImage(trayIcons[TrayIconStatus.Paused]); + break; + } + case TrayIconStatus.LowGas: { + this.setImage(trayIcons[TrayIconStatus.LowGas]); + break; + } + default: { + logger.electron('Unknown tray icon status:', status); + } + } + }); + }; +} + +/** + * Builds the context menu for the tray. + * @param {() => Electron.BrowserWindow | null} activeWindowCallback - A callback to retrieve the active window. + * @returns {Electron.Menu} The context menu for the tray. + */ +class PearlTrayContextMenu { + constructor(activeWindowCallback) { + return Electron.Menu.buildFromTemplate([ + { + label: 'Show app', + click: () => activeWindowCallback()?.show(), + }, + { + label: 'Hide app', + click: () => activeWindowCallback()?.hide(), + }, + { + label: 'Quit', + click: async () => { + Electron.app.quit(); + }, + }, + ]); + } +} + +module.exports = { PearlTray }; diff --git a/electron/icons.js b/electron/icons.js deleted file mode 100644 index 7c6c72e6..00000000 --- a/electron/icons.js +++ /dev/null @@ -1,30 +0,0 @@ -const { nativeImage } = require('electron'); - -const TRAY_ICONS_PATHS = { - LOGGED_OUT: `${__dirname}/assets/icons/tray-logged-out.png`, - LOW_GAS: `${__dirname}/assets/icons/tray-low-gas.png`, - PAUSED: `${__dirname}/assets/icons/tray-paused.png`, - RUNNING: `${__dirname}/assets/icons/tray-running.png`, -}; - -const TRAY_ICONS = { - LOGGED_OUT: nativeImage.createFromPath(TRAY_ICONS_PATHS.LOGGED_OUT), - LOW_GAS: nativeImage.createFromPath(TRAY_ICONS_PATHS.LOW_GAS), - PAUSED: nativeImage.createFromPath(TRAY_ICONS_PATHS.PAUSED), - RUNNING: nativeImage.createFromPath(TRAY_ICONS_PATHS.RUNNING), -}; - -try { - if (process.platform === 'darwin') { - // resize icons for macOS - const size = { width: 16, height: 16 }; - TRAY_ICONS.LOGGED_OUT = TRAY_ICONS.LOGGED_OUT.resize(size); - TRAY_ICONS.LOW_GAS = TRAY_ICONS.LOW_GAS.resize({ width: 16, height: 16 }); - TRAY_ICONS.PAUSED = TRAY_ICONS.PAUSED.resize({ width: 16, height: 16 }); - TRAY_ICONS.RUNNING = TRAY_ICONS.RUNNING.resize({ width: 16, height: 16 }); - } -} catch (e) { - console.log('Error resizing tray icons', e); -} - -module.exports = { TRAY_ICONS_PATHS, TRAY_ICONS }; diff --git a/electron/install.js b/electron/install.js index 634b2d53..d37d05f2 100644 --- a/electron/install.js +++ b/electron/install.js @@ -14,7 +14,7 @@ const homedir = os.homedir(); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc139'; +const OlasMiddlewareVersion = '0.1.0rc153'; const path = require('path'); const { app } = require('electron'); diff --git a/electron/main.js b/electron/main.js index 6752d08c..139bc2de 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,8 +1,6 @@ const { app, BrowserWindow, - Tray, - Menu, Notification, ipcMain, dialog, @@ -15,18 +13,17 @@ const os = require('os'); const next = require('next'); const http = require('http'); const AdmZip = require('adm-zip'); -const { TRAY_ICONS, TRAY_ICONS_PATHS } = require('./icons'); const { setupDarwin, setupUbuntu, setupWindows, Env } = require('./install'); const { paths } = require('./constants'); const { killProcesses } = require('./processes'); const { isPortAvailable, findAvailablePort } = require('./ports'); -const { PORT_RANGE, isWindows, isMac } = require('./constants'); -const { macUpdater } = require('./update'); +const { PORT_RANGE } = require('./constants'); const { setupStoreIpc } = require('./store'); const { logger } = require('./logger'); const { isDev } = require('./constants'); +const { PearlTray } = require('./components/PearlTray'); // Attempt to acquire the single instance lock const singleInstanceLock = app.requestSingleInstanceLock(); @@ -65,17 +62,17 @@ let appConfig = { }, }; -/** - * @type {BrowserWindow} - */ -let mainWindow; +/** @type {Electron.BrowserWindow | null} */ +let mainWindow = null; +/** @type {Electron.BrowserWindow | null} */ +let splashWindow = null; + +/** @type {Electron.Tray | null} */ +let tray = null; + +let operateDaemon, operateDaemonPid, nextAppProcess, nextAppProcessPid; -let tray, - splashWindow, - operateDaemon, - operateDaemonPid, - nextAppProcess, - nextAppProcessPid; +const getActiveWindow = () => splashWindow ?? mainWindow; function showNotification(title, body) { new Notification({ title, body }).show(); @@ -100,82 +97,10 @@ async function beforeQuit() { } tray?.destroy(); - mainWindow?.destroy(); splashWindow?.destroy(); + mainWindow?.destroy(); } -const getUpdatedTrayIcon = (iconPath) => { - const icon = iconPath; - if (icon.resize) { - icon.resize({ width: 16 }); - icon.setTemplateImage(true); - } - - return icon; -}; - -/** - * Creates the tray - */ -const createTray = () => { - const trayPath = getUpdatedTrayIcon( - isWindows || isMac ? TRAY_ICONS.LOGGED_OUT : TRAY_ICONS_PATHS.LOGGED_OUT, - ); - tray = new Tray(trayPath); - - const contextMenu = Menu.buildFromTemplate([ - { - label: 'Show app', - click: () => mainWindow?.show(), - }, - { - label: 'Hide app', - click: () => mainWindow?.hide(), - }, - { - label: 'Quit', - click: () => app.quit(), - }, - ]); - tray.setToolTip('Pearl'); - tray.setContextMenu(contextMenu); - - ipcMain.on('tray', (_event, status) => { - const isSupportedOS = isWindows || isMac; - switch (status) { - case 'low-gas': { - const icon = getUpdatedTrayIcon( - isSupportedOS ? TRAY_ICONS.LOW_GAS : TRAY_ICONS_PATHS.LOW_GAS, - ); - tray.setImage(icon); - break; - } - case 'running': { - const icon = getUpdatedTrayIcon( - isSupportedOS ? TRAY_ICONS.RUNNING : TRAY_ICONS_PATHS.RUNNING, - ); - tray.setImage(icon); - - break; - } - case 'paused': { - const icon = getUpdatedTrayIcon( - isSupportedOS ? TRAY_ICONS.PAUSED : TRAY_ICONS_PATHS.PAUSED, - ); - tray.setImage(icon); - break; - } - case 'logged-out': { - const icon = getUpdatedTrayIcon( - isSupportedOS ? TRAY_ICONS.LOGGED_OUT : TRAY_ICONS_PATHS.LOGGED_OUT, - ); - tray.setImage(icon); - break; - } - } - }); -}; - const APP_WIDTH = 460; /** @@ -228,23 +153,23 @@ const createMainWindow = async () => { mainWindow.setMenuBarVisibility(true); ipcMain.on('close-app', () => { - mainWindow.close(); + mainWindow?.close(); }); ipcMain.on('minimize-app', () => { - mainWindow.minimize(); + mainWindow?.minimize(); }); app.on('activate', () => { - if (mainWindow.isMinimized()) { - mainWindow.restore(); + if (mainWindow?.isMinimized()) { + mainWindow?.restore(); } else { - mainWindow.show(); + mainWindow?.show(); } }); ipcMain.on('set-height', (_event, height) => { - mainWindow.setSize(width, height); + mainWindow?.setSize(width, height); }); ipcMain.on('show-notification', (_event, title, description) => { @@ -517,7 +442,7 @@ ipcMain.on('check', async function (event, _argument) { event.sender.send('response', 'Launching App'); await createMainWindow(); - createTray(); + tray = new PearlTray(getActiveWindow); } catch (e) { logger.electron(e); new Notification({ @@ -565,11 +490,6 @@ app.once('ready', async () => { createSplashWindow(); }); -// UPDATER EVENTS -macUpdater.on('update-downloaded', () => { - macUpdater.quitAndInstall(); -}); - // PROCESS SPECIFIC EVENTS (HANDLES NON-GRACEFUL TERMINATION) process.on('uncaughtException', (error) => { logger.electron('Uncaught Exception:', error); diff --git a/electron/update.js b/electron/update.js index 3946655a..fe71d90b 100644 --- a/electron/update.js +++ b/electron/update.js @@ -2,15 +2,26 @@ const { publishOptions } = require('./constants'); const electronUpdater = require('electron-updater'); const logger = require('./logger'); -const macUpdater = new electronUpdater.MacUpdater({ +const updateOptions = { ...publishOptions, - channels: ['latest', 'beta', 'alpha'], // automatically update to all channels + // token is not required for macUpdater as repo is public, should overwrite it to undefined + token: undefined, + channels: ['latest', 'beta', 'alpha'], +}; + +const macUpdater = new electronUpdater.MacUpdater({ + ...updateOptions, }); -macUpdater.setFeedURL({ ...publishOptions }); +macUpdater.setFeedURL({ ...updateOptions }); -macUpdater.autoDownload = true; -macUpdater.autoInstallOnAppQuit = true; +macUpdater.autoDownload = false; +macUpdater.autoInstallOnAppQuit = false; macUpdater.logger = logger; +// UPDATER EVENTS +macUpdater.on('update-downloaded', () => { + macUpdater.quitAndInstall(); +}); + module.exports = { macUpdater }; diff --git a/frontend/components/AddressLink.tsx b/frontend/components/AddressLink.tsx new file mode 100644 index 00000000..83c4dfa9 --- /dev/null +++ b/frontend/components/AddressLink.tsx @@ -0,0 +1,25 @@ +import { UNICODE_SYMBOLS } from '@/constants/symbols'; +import { Address } from '@/types/Address'; +import { truncateAddress } from '@/utils/truncate'; + +type AddressLinkProps = { address?: Address; hideLinkArrow?: boolean }; + +export const AddressLink = ({ + address, + hideLinkArrow = false, +}: AddressLinkProps) => { + if (!address) return null; + + return ( + + {truncateAddress(address)} + + {hideLinkArrow ? null : ( + <> +   + {UNICODE_SYMBOLS.EXTERNAL_LINK} + + )} + + ); +}; diff --git a/frontend/components/InfoBreakdown.tsx b/frontend/components/InfoBreakdown.tsx index 6fff80bc..ed26b11e 100644 --- a/frontend/components/InfoBreakdown.tsx +++ b/frontend/components/InfoBreakdown.tsx @@ -37,7 +37,13 @@ const Line = styled.span<{ color?: Color }>` `1px solid ${color === 'primary' ? COLOR.PURPLE_LIGHT : COLOR.BORDER_GRAY}`}; `; -type Info = { id?: number | string; left: ReactNode; right: ReactNode }; +type Info = { + id?: number | string; + left: ReactNode; + leftClassName?: string; + right: ReactNode; + rightClassName?: string; +}; type InfoBreakdownListProps = { list: Info[]; parentStyle?: CSSProperties; @@ -54,9 +60,11 @@ export const InfoBreakdownList = ({ {list.map((item, index) => ( - {item.left} + {item.left} - {item.right} + + {item.right} + ))} diff --git a/frontend/components/InfoTooltip.tsx b/frontend/components/InfoTooltip.tsx new file mode 100644 index 00000000..54529d0d --- /dev/null +++ b/frontend/components/InfoTooltip.tsx @@ -0,0 +1,16 @@ +import { InfoCircleOutlined } from '@ant-design/icons'; +import Tooltip, { TooltipPlacement } from 'antd/es/tooltip'; + +import { COLOR } from '@/constants/colors'; + +export const InfoTooltip = ({ + placement = 'topLeft', + children, +}: { + placement?: TooltipPlacement; + children: React.ReactNode; +}) => ( + + + +); diff --git a/frontend/components/Layout/TopBar.tsx b/frontend/components/Layout/TopBar.tsx index ddaf1701..9428fc7f 100644 --- a/frontend/components/Layout/TopBar.tsx +++ b/frontend/components/Layout/TopBar.tsx @@ -35,8 +35,10 @@ const TrafficLights = styled.div` `; const TopBarContainer = styled.div` - position: sticky; + position: fixed; top: 0; + left: 0; + right: 0; z-index: 1; display: flex; align-items: center; diff --git a/frontend/components/Layout/index.tsx b/frontend/components/Layout/index.tsx index 42d6f1fd..42610c1b 100644 --- a/frontend/components/Layout/index.tsx +++ b/frontend/components/Layout/index.tsx @@ -32,6 +32,13 @@ const Container = styled.div<{ blur: 'true' | 'false' }>` `} `; +const Body = styled.div` + // check main.js for the height of the app ie, 700px + max-height: calc(700px - 45px); + padding-top: 45px; + overflow-y: auto; +`; + export const Layout = ({ children, }: PropsWithChildren & { vertical?: boolean }) => { @@ -53,7 +60,7 @@ export const Layout = ({ return ( - {children} + {children} ); }; diff --git a/frontend/components/MainPage/MainHeader/FirstRunModal.tsx b/frontend/components/MainPage/MainHeader/FirstRunModal.tsx index 73f017d7..ce75f568 100644 --- a/frontend/components/MainPage/MainHeader/FirstRunModal.tsx +++ b/frontend/components/MainPage/MainHeader/FirstRunModal.tsx @@ -2,6 +2,7 @@ import { Button, Flex, Modal, Typography } from 'antd'; import Image from 'next/image'; import { FC } from 'react'; +import { MODAL_WIDTH } from '@/constants/width'; import { useServiceTemplates } from '@/hooks/useServiceTemplates'; import { getMinimumStakedAmountRequired } from '@/utils/service'; @@ -19,7 +20,7 @@ export const FirstRunModal: FC = ({ open, onClose }) => { return ( ( arrow={false} title={ - Your agent earned rewards for this epoch and stopped working. It’ll - return to work once the next epoch starts. + Your agent earned rewards for this epoch, so decided to stop working + until the next epoch. } > @@ -222,7 +223,7 @@ const AgentNotRunningButton = () => { setServiceStatus(DeploymentStatus.DEPLOYED); // TODO: remove this workaround, middleware should respond when agent is staked & confirmed running after `createService` call - await new Promise((resolve) => setTimeout(resolve, 5000)); + await delayInSeconds(5); // update provider states sequentially // service id is required before activeStakingContractInfo & balances can be updated diff --git a/frontend/components/MainPage/header/LastTransaction.tsx b/frontend/components/MainPage/header/LastTransaction.tsx index 91595e09..f229d1ce 100644 --- a/frontend/components/MainPage/header/LastTransaction.tsx +++ b/frontend/components/MainPage/header/LastTransaction.tsx @@ -1,14 +1,25 @@ -import { Typography } from 'antd'; -import { useCallback, useState } from 'react'; +import { Skeleton, Typography } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { useInterval } from 'usehooks-ts'; import { useAddress } from '@/hooks/useAddress'; +import { usePageState } from '@/hooks/usePageState'; import { getLatestTransaction } from '@/service/Ethers'; import { TransactionInfo } from '@/types/TransactionInfo'; import { getTimeAgo } from '@/utils/time'; const { Text } = Typography; +const Loader = styled(Skeleton.Input)` + line-height: 1; + span { + width: 120px !important; + height: 12px !important; + margin-top: 6px !important; + } +`; + const POLLING_INTERVAL = 60 * 1000; // 1 minute /** @@ -16,6 +27,7 @@ const POLLING_INTERVAL = 60 * 1000; // 1 minute * by agent safe. */ export const LastTransaction = () => { + const { isPageLoadedAndOneMinutePassed } = usePageState(); const { multisigAddress } = useAddress(); const [isFetching, setIsFetching] = useState(true); @@ -35,7 +47,17 @@ export const LastTransaction = () => { // Poll for the latest transaction useInterval(() => fetchTransaction(), POLLING_INTERVAL); - if (isFetching) return null; + // Fetch the latest transaction on mount + useEffect(() => { + fetchTransaction(); + }, [fetchTransaction]); + + // Do not show the last transaction if the delay is not reached + if (!isPageLoadedAndOneMinutePassed) return null; + + if (isFetching) { + return ; + } if (!transaction) { return ( diff --git a/frontend/components/MainPage/modals/FirstRunModal.tsx b/frontend/components/MainPage/modals/FirstRunModal.tsx index 78de3d55..0c57b3b0 100644 --- a/frontend/components/MainPage/modals/FirstRunModal.tsx +++ b/frontend/components/MainPage/modals/FirstRunModal.tsx @@ -2,6 +2,7 @@ import { Button, Flex, Modal, Typography } from 'antd'; import Image from 'next/image'; import { FC } from 'react'; +import { MODAL_WIDTH } from '@/constants/width'; import { useServiceTemplates } from '@/hooks/useServiceTemplates'; import { getMinimumStakedAmountRequired } from '@/utils/service'; @@ -21,7 +22,7 @@ export const FirstRunModal: FC = ({ open, onClose }) => { return ( ` `; export const AddFundsSection = () => { + const fundSectionRef = useRef(null); const [isAddFundsVisible, setIsAddFundsVisible] = useState(false); + const addFunds = useCallback(async () => { + setIsAddFundsVisible(true); + + await delayInSeconds(0.1); + fundSectionRef?.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + const closeAddFunds = useCallback(() => setIsAddFundsVisible(false), []); + return ( <> @@ -57,12 +67,12 @@ export const AddFundsSection = () => { - {isAddFundsVisible && } + {isAddFundsVisible && } ); }; -export const OpenAddFundsSection = () => { +export const OpenAddFundsSection = forwardRef((_, ref) => { const { masterSafeAddress } = useWallet(); const truncatedFundingAddress: string | undefined = useMemo( @@ -79,7 +89,7 @@ export const OpenAddFundsSection = () => { [masterSafeAddress], ); return ( - <> + { handleCopy={handleCopyAddress} /> - + ); -}; +}); +OpenAddFundsSection.displayName = 'OpenAddFundsSection'; const AddFundsWarningAlertSection = () => ( diff --git a/frontend/components/MainPage/sections/NeedsFundsSection.tsx b/frontend/components/MainPage/sections/NeedsFundsSection.tsx index 71d0604f..b153611c 100644 --- a/frontend/components/MainPage/sections/NeedsFundsSection.tsx +++ b/frontend/components/MainPage/sections/NeedsFundsSection.tsx @@ -51,7 +51,6 @@ export const MainNeedsFunds = () => { )}
    -
  • Do not add more than these amounts.
  • Use the address in the “Add Funds” section below.
diff --git a/frontend/components/MainPage/sections/OlasBalanceSection.tsx b/frontend/components/MainPage/sections/OlasBalanceSection.tsx index 8b8dfd9a..45e5d8a5 100644 --- a/frontend/components/MainPage/sections/OlasBalanceSection.tsx +++ b/frontend/components/MainPage/sections/OlasBalanceSection.tsx @@ -1,15 +1,15 @@ -import { InfoCircleOutlined } from '@ant-design/icons'; -import { Button, Flex, Skeleton, Tooltip, Typography } from 'antd'; +import { RightOutlined } from '@ant-design/icons'; +import { Button, Flex, Skeleton, Typography } from 'antd'; import { useMemo } from 'react'; import styled from 'styled-components'; import { CustomAlert } from '@/components/Alert'; -import { InfoBreakdownList } from '@/components/InfoBreakdown'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; import { LOW_MASTER_SAFE_BALANCE } from '@/constants/thresholds'; +import { Pages } from '@/enums/PageState'; import { useBalance } from '@/hooks/useBalance'; import { useElectronApi } from '@/hooks/useElectronApi'; -import { useReward } from '@/hooks/useReward'; +import { usePageState } from '@/hooks/usePageState'; import { useStore } from '@/hooks/useStore'; import { balanceFormat } from '@/utils/numberFormatters'; @@ -21,59 +21,6 @@ const Balance = styled.span` margin-right: 4px; `; -const OVERLAY_STYLE = { maxWidth: '300px', width: '300px' }; - -const CurrentBalance = () => { - const { totalOlasBalance, totalOlasStakedBalance } = useBalance(); - const { accruedServiceStakingRewards } = useReward(); - - const balances = useMemo(() => { - return [ - { - title: 'Staked amount', - value: balanceFormat(totalOlasStakedBalance ?? 0, 2), - }, - { - title: 'Unclaimed rewards', - value: balanceFormat(accruedServiceStakingRewards ?? 0, 2), - }, - { - // Unused funds should only be ‘free-floating’ OLAS that is neither unclaimed nor staked. - title: 'Unused funds', - value: balanceFormat( - (totalOlasBalance ?? 0) - - (totalOlasStakedBalance ?? 0) - - (accruedServiceStakingRewards ?? 0), - 2, - ), - }, - ]; - }, [accruedServiceStakingRewards, totalOlasBalance, totalOlasStakedBalance]); - - return ( - - Current balance  - ({ - left: item.title, - right: `${item.value} OLAS`, - }))} - size="small" - parentStyle={{ padding: 4, gap: 8 }} - /> - } - > - - - - ); -}; - const MainOlasBalanceAlert = styled.div` .ant-alert { margin-bottom: 8px; @@ -111,8 +58,8 @@ const LowTradingBalanceAlert = () => { {`To run your agent, add at least $${LOW_MASTER_SAFE_BALANCE} XDAI to your account.`} - Do it quickly to avoid your agent missing its targets and getting - suspended! + Your agent is at risk of missing its targets, which would result + in several days' suspension. } @@ -136,10 +83,10 @@ const AvoidSuspensionAlert = () => { Avoid suspension! - Run your agent for at least half an hour a day to make sure it - hits its targets. If it misses its targets 2 days in a row, it’ll - be suspended. You won’t be able to run it or earn rewards for - several days. + Run your agent for at least half an hour a day to avoid missing + targets. If it misses its targets 2 days in a row, it’ll be + suspended. You won’t be able to run it or earn rewards for several + days. - {data ? ( @@ -212,7 +185,7 @@ export const DebugInfoSection = () => { )} - +
); }; diff --git a/frontend/components/SetupPage/Create/SetupBackupSigner.tsx b/frontend/components/SetupPage/Create/SetupBackupSigner.tsx index edab5bfa..08a9f677 100644 --- a/frontend/components/SetupPage/Create/SetupBackupSigner.tsx +++ b/frontend/components/SetupPage/Create/SetupBackupSigner.tsx @@ -38,9 +38,9 @@ export const SetupBackupSigner = () => { Set backup wallet - To keep your funds safe, we encourage you to add one of your existing - crypto wallets as a backup. This enables you to recover your funds if - you lose both your password and seed phrase. + To help keep your funds safe, we encourage you to add one of your + existing crypto wallets as a backup. You may recover your funds to + your backup wallet if you lose both your password and seed phrase. diff --git a/frontend/components/SetupPage/SetupRestore.tsx b/frontend/components/SetupPage/SetupRestore.tsx index 99f57d61..da04e9f5 100644 --- a/frontend/components/SetupPage/SetupRestore.tsx +++ b/frontend/components/SetupPage/SetupRestore.tsx @@ -50,7 +50,7 @@ export const SetupRestoreMain = () => { If you don’t have the seed phrase but added a backup wallet to your - account, you can still restore your funds, but you won’t be able to + account, you may still restore your funds, but you won’t be able to recover access to your Pearl account.