diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bfe42ac75..044325707 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -93,7 +93,7 @@ jobs:
- run: yarn install-deps
- name: "Build frontend with env vars"
run: yarn build:frontend
- env:
+ env:
NODE_ENV: production
DEV_RPC: https://rpc-gate.autonolas.tech/gnosis-rpc/
IS_STAGING: ${{ github.ref != 'refs/heads/main' && 'true' || 'false' }}
@@ -110,4 +110,26 @@ jobs:
NODE_ENV: production
DEV_RPC: https://rpc-gate.autonolas.tech/gnosis-rpc/
FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/
- run: node build.js
\ No newline at end of file
+ run: node build.js
+ - name: "Build frontend with dev env vars"
+ run: yarn build:frontend
+ env:
+ NODE_ENV: development
+ DEV_RPC: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
+ IS_STAGING: ${{ github.ref != 'refs/heads/main' && 'true' || 'false' }}
+ FORK_URL: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
+ - name: "Build, notarize, publish dev build"
+ env:
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
+ APPLE_ID: ${{ secrets.APPLEID }}
+ APPLETEAMID: ${{ secrets.APPLETEAMID }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ GH_TOKEN: ${{ secrets.github_token}}
+ NODE_ENV: development
+ DEV_RPC: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
+ FORK_URL: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
+ run: |
+ echo "DEV_RPC=https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b" >> .env
+ echo -e "FORK_URL=https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b" >> .env
+ node build.js
\ No newline at end of file
diff --git a/build.js b/build.js
index a7e15a32b..fa1e1b0db 100644
--- a/build.js
+++ b/build.js
@@ -6,6 +6,16 @@ const build = require('electron-builder').build;
const { publishOptions } = require('./electron/constants');
+/**
+ * Get the artifact name for the build based on the environment.
+ * @returns {string}
+ */
+function artifactName() {
+ const env = process.env.NODE_ENV;
+ const prefix = env === 'production' ? '' : 'dev-';
+ return prefix + '${productName}-${version}-${platform}-${arch}.${ext}';
+}
+
const main = async () => {
console.log('Building...');
@@ -14,7 +24,7 @@ const main = async () => {
publish: 'onTag',
config: {
appId: 'xyz.valory.olas-operate-app',
- artifactName: '${productName}-${version}-${platform}-${arch}.${ext}',
+ artifactName: artifactName(),
productName: 'Pearl',
files: ['electron/**/*', 'package.json'],
directories: {
@@ -26,6 +36,10 @@ const main = async () => {
to: 'bins',
filter: ['**/*'],
},
+ {
+ from: '.env',
+ to: '.env'
+ },
],
cscKeyPassword: process.env.CSC_KEY_PASSWORD,
cscLink: process.env.CSC_LINK,
diff --git a/build_pearl.sh b/build_pearl.sh
new file mode 100755
index 000000000..65e5d4ac9
--- /dev/null
+++ b/build_pearl.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# ------------------------------------------------------------------------------
+#
+# Copyright 2023-2024 Valory AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# ------------------------------------------------------------------------------
+
+cd "$(dirname "$0")"
+
+BIN_DIR="electron/bins/"
+mkdir -p $BIN_DIR
+
+poetry install
+
+poetry run pyinstaller operate/services/utils/tendermint.py --onefile --distpath $BIN_DIR
+
+poetry run pyinstaller \
+ --collect-data eth_account \
+ --collect-all aea \
+ --collect-all autonomy \
+ --collect-all operate \
+ --collect-all aea_ledger_ethereum \
+ --collect-all aea_ledger_cosmos \
+ --collect-all aea_ledger_ethereum_flashbots \
+ --hidden-import aea_ledger_ethereum \
+ --hidden-import aea_ledger_cosmos \
+ --hidden-import aea_ledger_ethereum_flashbots \
+ operate/pearl.py \
+ --add-binary ${BIN_DIR}/aea_bin_x64:. \
+ --add-binary ${BIN_DIR}/aea_bin_arm64:. \
+ --onefile \
+ --distpath $BIN_DIR \
+ --name pearl_$(uname -m)
+
diff --git a/download_binaries.sh b/download_binaries.sh
new file mode 100755
index 000000000..db12c6223
--- /dev/null
+++ b/download_binaries.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+BIN_DIR="electron/bins/"
+mkdir -p $BIN_DIR
+
+trader_version=$(poetry run python -c "import yaml; config = yaml.safe_load(open('templates/trader.yaml')); print(config['configuration']['trader_version'])")
+
+curl -L -o "${BIN_DIR}aea_bin_x64" "https://github.com/valory-xyz/trader/releases/download/${trader_version}/trader_bin_x64"
+
+curl -L -o "${BIN_DIR}aea_bin_arm64" "https://github.com/valory-xyz/trader/releases/download/${trader_version}/trader_bin_arm64"
diff --git a/electron/install.js b/electron/install.js
index ac60b76b3..9da1dcad6 100644
--- a/electron/install.js
+++ b/electron/install.js
@@ -14,7 +14,17 @@ const { paths } = require('./constants');
* - 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.0rc110';
+const OlasMiddlewareVersion = '0.1.0rc111';
+
+const path = require('path');
+const { app } = require('electron');
+
+// load env vars
+require('dotenv').config({
+ path: app.isPackaged
+ ? path.join(process.resourcesPath, '.env')
+ : path.resolve(process.cwd(), '.env'),
+});
const Env = {
...process.env,
diff --git a/electron/main.js b/electron/main.js
index e2c66a668..5a01cae73 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -131,17 +131,18 @@ const createTray = () => {
tray.setContextMenu(contextMenu);
ipcMain.on('tray', (_event, status) => {
+ const isSupportedOS = isWindows || isMac;
switch (status) {
case 'low-gas': {
const icon = getUpdatedTrayIcon(
- isWindows || isMac ? TRAY_ICONS.LOW_GAS : TRAY_ICONS_PATHS.LOW_GAS,
+ isSupportedOS ? TRAY_ICONS.LOW_GAS : TRAY_ICONS_PATHS.LOW_GAS,
);
tray.setImage(icon);
break;
}
case 'running': {
const icon = getUpdatedTrayIcon(
- isWindows || isMac ? TRAY_ICONS.RUNNING : TRAY_ICONS_PATHS.RUNNING,
+ isSupportedOS ? TRAY_ICONS.RUNNING : TRAY_ICONS_PATHS.RUNNING,
);
tray.setImage(icon);
@@ -149,7 +150,14 @@ const createTray = () => {
}
case 'paused': {
const icon = getUpdatedTrayIcon(
- isWindows || isMac ? TRAY_ICONS.PAUSED : TRAY_ICONS_PATHS.PAUSED,
+ 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;
diff --git a/electron/store.js b/electron/store.js
index 5577decff..afbc7f62d 100644
--- a/electron/store.js
+++ b/electron/store.js
@@ -4,6 +4,7 @@ const defaultSchema = {
isInitialFunded: { type: 'boolean', default: false },
firstStakingRewardAchieved: { type: 'boolean', default: false },
firstRewardNotificationShown: { type: 'boolean', default: false },
+ agentEvictionAlertShown: { type: 'boolean', default: false },
};
const setupStoreIpc = async (ipcChannel, mainWindow, storeInitialValues) => {
diff --git a/frontend/components/Main/MainGasBalance.tsx b/frontend/components/Main/MainGasBalance.tsx
index 3f8e77487..0d1f83f26 100644
--- a/frontend/components/Main/MainGasBalance.tsx
+++ b/frontend/components/Main/MainGasBalance.tsx
@@ -1,11 +1,13 @@
import { ArrowUpOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { Skeleton, Tooltip, Typography } from 'antd';
-import { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { COLOR } from '@/constants/colors';
import { LOW_BALANCE } from '@/constants/thresholds';
import { useBalance } from '@/hooks/useBalance';
+import { useElectronApi } from '@/hooks/useElectronApi';
+import { useStore } from '@/hooks/useStore';
import { useWallet } from '@/hooks/useWallet';
import { CardSection } from '../styled/CardSection';
@@ -30,20 +32,43 @@ const EmptyDot = styled(Dot)`
const FineDot = styled(Dot)`
background-color: ${COLOR.GREEN_2};
`;
-const LowDot = styled(Dot)`
- background-color: ${COLOR.ORANGE};
-`;
const BalanceStatus = () => {
- const { safeBalance } = useBalance();
+ const { isBalanceLoaded, safeBalance } = useBalance();
+ const { storeState } = useStore();
+ const { showNotification } = useElectronApi();
- const status = useMemo(() => {
- if (!safeBalance || safeBalance.ETH === 0) {
- return { statusName: 'Empty', StatusComponent: EmptyDot };
+ const [isLowBalanceNotificationShown, setIsLowBalanceNotificationShown] =
+ useState(false);
+
+ // show notification if balance is too low
+ useEffect(() => {
+ if (!isBalanceLoaded) return;
+ if (!safeBalance) return;
+ if (!showNotification) return;
+ if (!storeState?.isInitialFunded) return;
+
+ if (safeBalance.ETH < LOW_BALANCE && !isLowBalanceNotificationShown) {
+ showNotification('Trading balance is too low.');
+ setIsLowBalanceNotificationShown(true);
}
- if (safeBalance.ETH < LOW_BALANCE) {
- return { statusName: 'Low', StatusComponent: LowDot };
+ // If it has already been shown and the balance has increased,
+ // should show the notification again if it goes below the threshold.
+ if (safeBalance.ETH >= LOW_BALANCE && isLowBalanceNotificationShown) {
+ setIsLowBalanceNotificationShown(false);
+ }
+ }, [
+ isBalanceLoaded,
+ isLowBalanceNotificationShown,
+ safeBalance,
+ showNotification,
+ storeState?.isInitialFunded,
+ ]);
+
+ const status = useMemo(() => {
+ if (!safeBalance || safeBalance.ETH < LOW_BALANCE) {
+ return { statusName: 'Too low', StatusComponent: EmptyDot };
}
return { statusName: 'Fine', StatusComponent: FineDot };
diff --git a/frontend/components/Main/MainHeader/AgentButton/index.tsx b/frontend/components/Main/MainHeader/AgentButton/index.tsx
index cd6f72c59..356a45c12 100644
--- a/frontend/components/Main/MainHeader/AgentButton/index.tsx
+++ b/frontend/components/Main/MainHeader/AgentButton/index.tsx
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react';
import { Chain, DeploymentStatus } from '@/client';
import { COLOR } from '@/constants/colors';
+import { LOW_BALANCE } from '@/constants/thresholds';
import { useBalance } from '@/hooks/useBalance';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useServices } from '@/hooks/useServices';
@@ -15,7 +16,10 @@ import { ServicesService } from '@/service/Services';
import { WalletService } from '@/service/Wallet';
import { getMinimumStakedAmountRequired } from '@/utils/service';
-import { CannotStartAgent } from '../CannotStartAgent';
+import {
+ CannotStartAgent,
+ CannotStartAgentDueToUnexpectedError,
+} from '../CannotStartAgent';
import { requiredGas, requiredOlas } from '../constants';
const { Text } = Typography;
@@ -185,6 +189,15 @@ const AgentNotRunningButton = () => {
]);
const isDeployable = useMemo(() => {
+ // if the agent is NOT running and the balance is too low,
+ // user should not be able to start the agent
+ const isServiceInactive =
+ serviceStatus === DeploymentStatus.BUILT ||
+ serviceStatus === DeploymentStatus.STOPPED;
+ if (isServiceInactive && safeBalance && safeBalance.ETH < LOW_BALANCE) {
+ return false;
+ }
+
if (serviceStatus === DeploymentStatus.DEPLOYED) return false;
if (serviceStatus === DeploymentStatus.DEPLOYING) return false;
if (serviceStatus === DeploymentStatus.STOPPING) return false;
@@ -211,6 +224,7 @@ const AgentNotRunningButton = () => {
serviceStatus,
storeState?.isInitialFunded,
totalEthBalance,
+ safeBalance,
]);
const buttonProps: ButtonProps = {
@@ -258,11 +272,7 @@ export const AgentButton = () => {
return