Skip to content

Commit f7fcd5e

Browse files
authored
feat: Inform users that there are no available slots on staking contract and prompt them to switch (#421)
* fix: improve error messages in AgentButton component * feat: add NoAvailableSlotsOnTheContract component * feat: integrate NoAvailableSlotsOnTheContract component and enhance service slot checks * feat: implement useCanStartUpdateStakingContract hook and update related components * feat: add available slots display and max-width styling to StakingContractDetails * feat: add POPOVER_WIDTH_LARGE constant and apply max-width styling to CountdownUntilMigration component * fix: correct spelling of 'occurred' in error notifications in AgentButton component * refactor: rename useCanStartUpdateStakingContract hook to useCanUpdateStakingContract and update related components * feat: update StakingContractUpdate and NoAvailableSlotsOnTheContract components to accept stakingProgramId prop and remove useCanUpdateStakingContract hook * chore: bump version to 0.1.0-rc186 in package.json and pyproject.toml
1 parent 7c0d5b8 commit f7fcd5e

File tree

14 files changed

+120
-21
lines changed

14 files changed

+120
-21
lines changed

frontend/components/MainPage/header/AgentButton.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const AgentRunningButton = () => {
8989
await ServicesService.stopDeployment(service.hash);
9090
} catch (error) {
9191
console.error(error);
92-
showNotification?.('Error while stopping agent');
92+
showNotification?.('Some error occurred while stopping agent');
9393
} finally {
9494
// Resume polling, will update to correct status regardless of success
9595
setIsServicePollingPaused(false);
@@ -145,6 +145,7 @@ const AgentNotRunningButton = () => {
145145
isAgentEvicted,
146146
setIsPaused: setIsStakingContractInfoPollingPaused,
147147
updateActiveStakingContractInfo,
148+
hasEnoughServiceSlots,
148149
} = useStakingContractInfo();
149150
const { activeStakingProgramId, defaultStakingProgramId } =
150151
useStakingProgram();
@@ -192,7 +193,7 @@ const AgentNotRunningButton = () => {
192193
} catch (error) {
193194
console.error(error);
194195
setServiceStatus(undefined);
195-
showNotification?.('Error while creating safe');
196+
showNotification?.('Some error occurred while creating safe');
196197
setIsStakingContractInfoPollingPaused(false);
197198
setIsServicePollingPaused(false);
198199
setIsBalancePollingPaused(false);
@@ -211,7 +212,7 @@ const AgentNotRunningButton = () => {
211212
} catch (error) {
212213
console.error(error);
213214
setServiceStatus(undefined);
214-
showNotification?.('Error while deploying service');
215+
showNotification?.('Some error occurred while deploying service');
215216
setIsServicePollingPaused(false);
216217
setIsBalancePollingPaused(false);
217218
setIsStakingContractInfoPollingPaused(false);
@@ -223,7 +224,9 @@ const AgentNotRunningButton = () => {
223224
showNotification?.(`Your agent is running!`);
224225
} catch (error) {
225226
console.error(error);
226-
showNotification?.('Error while showing "running" notification');
227+
showNotification?.(
228+
'Some error occurred while showing "running" notification',
229+
);
227230
}
228231

229232
// Can assume successful deployment
@@ -278,6 +281,9 @@ const AgentNotRunningButton = () => {
278281

279282
if (!requiredOlas) return false;
280283

284+
// If no slots available, agent cannot be started
285+
if (!hasEnoughServiceSlots) return false;
286+
281287
// case where service exists & user has initial funded
282288
if (service && storeState?.isInitialFunded) {
283289
if (!safeOlasBalanceWithStaked) return false;
@@ -302,6 +308,7 @@ const AgentNotRunningButton = () => {
302308
requiredOlas,
303309
totalEthBalance,
304310
isLowBalance,
311+
hasEnoughServiceSlots,
305312
]);
306313

307314
const buttonProps: ButtonProps = {

frontend/components/MainPage/index.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useBalance } from '@/hooks/useBalance';
88
import { useMasterSafe } from '@/hooks/useMasterSafe';
99
import { usePageState } from '@/hooks/usePageState';
1010
import { useServices } from '@/hooks/useServices';
11+
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
1112
import { useStakingProgram } from '@/hooks/useStakingProgram';
1213

1314
import { MainHeader } from './header';
@@ -26,6 +27,7 @@ export const Main = () => {
2627
const { updateServicesState } = useServices();
2728
const { updateBalances, isLoaded, setIsLoaded } = useBalance();
2829
const { activeStakingProgramId: currentStakingProgram } = useStakingProgram();
30+
const { hasEnoughServiceSlots } = useStakingContractInfo();
2931

3032
useEffect(() => {
3133
if (!isLoaded) {
@@ -37,6 +39,7 @@ export const Main = () => {
3739
const hideMainOlasBalanceTopBorder = [
3840
!backupSafeAddress,
3941
currentStakingProgram === StakingProgramId.Alpha,
42+
!hasEnoughServiceSlots,
4043
].some((condition) => !!condition);
4144

4245
return (
@@ -71,7 +74,9 @@ export const Main = () => {
7174
<MainOlasBalance isBorderTopVisible={!hideMainOlasBalanceTopBorder} />
7275
<RewardsSection />
7376
<KeepAgentRunningSection />
74-
<StakingContractUpdate />
77+
{currentStakingProgram && (
78+
<StakingContractUpdate stakingProgramId={currentStakingProgram} />
79+
)}
7580
<GasBalanceSection />
7681
<MainNeedsFunds />
7782
<AddFundsSection />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Flex, Typography } from 'antd';
2+
3+
import { useMigrate } from '@/components/ManageStakingPage/StakingContractSection/useMigrate';
4+
import { Pages } from '@/enums/PageState';
5+
import { StakingProgramId } from '@/enums/StakingProgram';
6+
import { usePageState } from '@/hooks/usePageState';
7+
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
8+
9+
import { CustomAlert } from '../../../Alert';
10+
11+
const { Text } = Typography;
12+
13+
type NoAvailableSlotsOnTheContractProps = {
14+
stakingProgramId: StakingProgramId;
15+
};
16+
export const NoAvailableSlotsOnTheContract = ({
17+
stakingProgramId,
18+
}: NoAvailableSlotsOnTheContractProps) => {
19+
const { goto } = usePageState();
20+
const { hasEnoughServiceSlots } = useStakingContractInfo();
21+
const { canUpdateStakingContract } = useMigrate(stakingProgramId);
22+
23+
if (hasEnoughServiceSlots) return null;
24+
25+
return (
26+
<CustomAlert
27+
type="warning"
28+
fullWidth
29+
showIcon
30+
message={
31+
<Flex justify="space-between" gap={4} vertical>
32+
<Text className="font-weight-600">
33+
No available slots on the contract
34+
</Text>
35+
<span className="text-sm">
36+
Select a contract with available slots to be able to start your
37+
agent.
38+
</span>
39+
{canUpdateStakingContract && (
40+
<Text
41+
className="pointer hover-underline text-primary text-sm"
42+
onClick={() => goto(Pages.ManageStaking)}
43+
>
44+
Change staking contract
45+
</Text>
46+
)}
47+
</Flex>
48+
}
49+
/>
50+
);
51+
};
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import { CardSection } from '@/components/styled/CardSection';
2+
import { useStakingProgram } from '@/hooks/useStakingProgram';
23

34
import { AddBackupWalletAlert } from './AddBackupWalletAlert';
45
import { AvoidSuspensionAlert } from './AvoidSuspensionAlert';
56
import { LowTradingBalanceAlert } from './LowTradingBalanceAlert';
67
import { NewStakingProgramAlert } from './NewStakingProgramAlert';
8+
import { NoAvailableSlotsOnTheContract } from './NoAvailableSlotsOnTheContract';
79
import { UpdateAvailableAlert } from './UpdateAvailableAlert';
810

911
export const AlertSections = () => {
12+
const { activeStakingProgramId } = useStakingProgram();
13+
1014
return (
1115
<CardSection vertical>
1216
<UpdateAvailableAlert />
1317
<AddBackupWalletAlert />
1418
<NewStakingProgramAlert />
1519
<AvoidSuspensionAlert />
1620
<LowTradingBalanceAlert />
21+
{activeStakingProgramId && (
22+
<NoAvailableSlotsOnTheContract
23+
stakingProgramId={activeStakingProgramId}
24+
/>
25+
)}
1726
</CardSection>
1827
);
1928
};

frontend/components/MainPage/sections/StakingContractUpdate.tsx

+7-12
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,32 @@ import { useMemo } from 'react';
44

55
import { STAKING_PROGRAM_META } from '@/constants/stakingProgramMeta';
66
import { Pages } from '@/enums/PageState';
7-
import { useBalance } from '@/hooks/useBalance';
8-
import { useNeedsFunds } from '@/hooks/useNeedsFunds';
7+
import { StakingProgramId } from '@/enums/StakingProgram';
98
import { usePageState } from '@/hooks/usePageState';
109
import { useStakingProgram } from '@/hooks/useStakingProgram';
1110

11+
import { useMigrate } from '../../ManageStakingPage/StakingContractSection/useMigrate';
1212
import { CardSection } from '../../styled/CardSection';
1313

1414
const { Text } = Typography;
1515

16-
export const StakingContractUpdate = () => {
16+
type StakingContractUpdateProps = { stakingProgramId: StakingProgramId };
17+
export const StakingContractUpdate = ({
18+
stakingProgramId,
19+
}: StakingContractUpdateProps) => {
1720
const { goto } = usePageState();
18-
const { isBalanceLoaded, isLowBalance } = useBalance();
19-
const { needsInitialFunding } = useNeedsFunds();
2021
const {
2122
activeStakingProgramMeta,
2223
isActiveStakingProgramLoaded,
2324
defaultStakingProgramId,
2425
} = useStakingProgram();
26+
const { canUpdateStakingContract } = useMigrate(stakingProgramId);
2527

2628
const stakingContractName = useMemo(() => {
2729
if (activeStakingProgramMeta) return activeStakingProgramMeta.name;
2830
return STAKING_PROGRAM_META[defaultStakingProgramId].name;
2931
}, [activeStakingProgramMeta, defaultStakingProgramId]);
3032

31-
const canUpdateStakingContract = useMemo(() => {
32-
if (!isBalanceLoaded) return false;
33-
if (isLowBalance) return false;
34-
if (needsInitialFunding) return false;
35-
return true;
36-
}, [isBalanceLoaded, isLowBalance, needsInitialFunding]);
37-
3833
const stakingButton = useMemo(() => {
3934
if (!isActiveStakingProgramLoaded) return <Skeleton.Input />;
4035
return (

frontend/components/ManageStakingPage/StakingContractSection/CountdownUntilMigration.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isNil } from 'lodash';
33
import { useState } from 'react';
44
import { useInterval } from 'usehooks-ts';
55

6+
import { POPOVER_WIDTH_LARGE } from '@/constants/width';
67
import { StakingContractInfo } from '@/types/Autonolas';
78

89
const { Text } = Typography;
@@ -41,7 +42,7 @@ export const CountdownUntilMigration = ({
4142
: countdownDisplayFormat(secondsUntilReady);
4243

4344
return (
44-
<Flex vertical gap={1}>
45+
<Flex vertical gap={1} style={{ maxWidth: POPOVER_WIDTH_LARGE }}>
4546
<Text strong>Can&apos;t switch because you unstaked too recently.</Text>
4647
<Text>This may be because your agent was suspended.</Text>
4748
<Text>Keep running your agent and you&apos;ll be able to switch in</Text>

frontend/components/ManageStakingPage/StakingContractSection/StakingContractDetails.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Alert, Skeleton } from 'antd';
22
import { useMemo } from 'react';
33

44
import { InfoBreakdownList } from '@/components/InfoBreakdown';
5+
import { NA } from '@/constants/symbols';
56
import { StakingProgramId } from '@/enums/StakingProgram';
67
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
78

@@ -19,13 +20,18 @@ export const StakingContractDetails = ({
1920

2021
const details = stakingContractInfoRecord[stakingProgramId];
2122
return [
23+
{
24+
left: 'Available slots',
25+
right: details.maxNumServices || NA,
26+
},
2227
{
2328
left: 'Rewards per epoch',
2429
right: `~ ${details.rewardsPerWorkPeriod?.toFixed(2)} OLAS`,
2530
},
2631
{
2732
left: 'Estimated Annual Percentage Yield (APY)',
2833
right: `${details.apy}%`,
34+
leftClassName: 'max-width-200',
2935
},
3036
{
3137
left: 'Required OLAS for staking',

frontend/components/ManageStakingPage/StakingContractSection/useMigrate.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
44
import { DeploymentStatus } from '@/client';
55
import { StakingProgramId } from '@/enums/StakingProgram';
66
import { useBalance } from '@/hooks/useBalance';
7+
import { useNeedsFunds } from '@/hooks/useNeedsFunds';
78
import { useServices } from '@/hooks/useServices';
89
import { useServiceTemplates } from '@/hooks/useServiceTemplates';
910
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
@@ -40,16 +41,19 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
4041
isBalanceLoaded,
4142
masterSafeBalance: safeBalance,
4243
totalOlasStakedBalance,
44+
isLowBalance,
4345
} = useBalance();
4446
const { activeStakingProgramId, activeStakingProgramMeta } =
4547
useStakingProgram();
48+
const { needsInitialFunding } = useNeedsFunds();
4649

4750
const {
4851
activeStakingContractInfo,
4952
isServiceStaked,
5053
isServiceStakedForMinimumDuration,
5154
isStakingContractInfoLoaded,
5255
stakingContractInfoRecord,
56+
hasEnoughServiceSlots,
5357
} = useStakingContractInfo();
5458

5559
const stakingContractInfo = stakingContractInfoRecord?.[stakingProgramId];
@@ -231,8 +235,22 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
231235
stakingProgramId,
232236
]);
233237

238+
const canUpdateStakingContract = useMemo(() => {
239+
if (!isBalanceLoaded) return false;
240+
if (isLowBalance) return false;
241+
if (needsInitialFunding) return false;
242+
if (!hasEnoughServiceSlots) return false;
243+
return true;
244+
}, [
245+
isBalanceLoaded,
246+
isLowBalance,
247+
needsInitialFunding,
248+
hasEnoughServiceSlots,
249+
]);
250+
234251
return {
235252
migrateValidation,
236253
firstDeployValidation,
254+
canUpdateStakingContract,
237255
};
238256
};

frontend/constants/symbols.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export const NA = 'n/a';
2+
13
export const UNICODE_SYMBOLS = {
24
OLAS: '☴',
35
EXTERNAL_LINK: '↗',

frontend/constants/width.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const MODAL_WIDTH = 412;
22

33
export const POPOVER_WIDTH_MEDIUM = 260;
4+
5+
export const POPOVER_WIDTH_LARGE = 340;

frontend/styles/globals.scss

+3-1
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,12 @@ ul.alert-list {
262262
.w-3\/4 {
263263
width: 75% !important;
264264
}
265-
266265
.w-full {
267266
width: 100% !important;
268267
}
268+
.max-width-200 {
269+
max-width: 200px;
270+
}
269271

270272
.loading-ellipses:after {
271273
overflow: hidden;

frontend/types/Autonolas.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type StakingRewardsInfo = {
1212

1313
export type StakingContractInfo = {
1414
availableRewards: number;
15+
/* number of slots available for staking */
1516
maxNumServices: number;
1617
serviceIds: number[];
1718
/** minimum staking duration (in seconds) */

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@
6363
"download-binaries": "sh download_binaries.sh",
6464
"build:pearl": "sh build_pearl.sh"
6565
},
66-
"version": "0.1.0-rc184"
66+
"version": "0.1.0-rc186"
6767
}

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "olas-operate-middleware"
3-
version = "0.1.0-rc184"
3+
version = "0.1.0-rc186"
44
description = ""
55
authors = ["David Vilela <[email protected]>", "Viraj Patel <[email protected]>"]
66
readme = "README.md"

0 commit comments

Comments
 (0)