Skip to content

Commit bcf4ea1

Browse files
authored
Merge pull request #402 from valory-xyz/feat/staking_program_marketplace
Mech marketplace integration and staking contract
2 parents 4056077 + d8cf13c commit bcf4ea1

37 files changed

+1013
-500
lines changed

frontend/abis/mechMarketplace.ts

+442
Large diffs are not rendered by default.
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export const REQUESTER_ACTIVITY_CHECKER_ABI = [
2+
{
3+
inputs: [
4+
{ internalType: 'address', name: '_mechMarketplace', type: 'address' },
5+
{ internalType: 'uint256', name: '_livenessRatio', type: 'uint256' },
6+
],
7+
stateMutability: 'nonpayable',
8+
type: 'constructor',
9+
},
10+
{ inputs: [], name: 'ZeroAddress', type: 'error' },
11+
{ inputs: [], name: 'ZeroValue', type: 'error' },
12+
{
13+
inputs: [{ internalType: 'address', name: 'multisig', type: 'address' }],
14+
name: 'getMultisigNonces',
15+
outputs: [{ internalType: 'uint256[]', name: 'nonces', type: 'uint256[]' }],
16+
stateMutability: 'view',
17+
type: 'function',
18+
},
19+
{
20+
inputs: [
21+
{ internalType: 'uint256[]', name: 'curNonces', type: 'uint256[]' },
22+
{ internalType: 'uint256[]', name: 'lastNonces', type: 'uint256[]' },
23+
{ internalType: 'uint256', name: 'ts', type: 'uint256' },
24+
],
25+
name: 'isRatioPass',
26+
outputs: [{ internalType: 'bool', name: 'ratioPass', type: 'bool' }],
27+
stateMutability: 'view',
28+
type: 'function',
29+
},
30+
{
31+
inputs: [],
32+
name: 'livenessRatio',
33+
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
34+
stateMutability: 'view',
35+
type: 'function',
36+
},
37+
{
38+
inputs: [],
39+
name: 'mechMarketplace',
40+
outputs: [{ internalType: 'address', name: '', type: 'address' }],
41+
stateMutability: 'view',
42+
type: 'function',
43+
},
44+
];

frontend/client/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export type ConfigurationTemplate = {
6767
agent_id: number;
6868
threshold: number;
6969
use_staking: boolean;
70+
use_mech_marketplace: boolean;
7071
cost_of_bond: number;
7172
monthly_gas_estimate: number;
7273
fund_requirements: FundRequirementsTemplate;

frontend/components/MainPage/header/AgentButton.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react';
44

55
import { Chain, DeploymentStatus } from '@/client';
66
import { COLOR } from '@/constants/colors';
7+
import { StakingProgramId } from '@/enums/StakingProgram';
78
import { useBalance } from '@/hooks/useBalance';
89
import { useElectronApi } from '@/hooks/useElectronApi';
910
import { useReward } from '@/hooks/useReward';
@@ -132,7 +133,7 @@ const AgentNotRunningButton = () => {
132133
const { showNotification } = useElectronApi();
133134
const {
134135
setIsPaused: setIsBalancePollingPaused,
135-
safeBalance,
136+
masterSafeBalance: safeBalance,
136137
isLowBalance,
137138
totalOlasStakedBalance,
138139
totalEthBalance,
@@ -179,6 +180,10 @@ const AgentNotRunningButton = () => {
179180
// Mock "DEPLOYING" status (service polling will update this once resumed)
180181
setServiceStatus(DeploymentStatus.DEPLOYING);
181182

183+
// Get the active staking program id; default id if there's no agent yet
184+
const stakingProgramId: StakingProgramId =
185+
activeStakingProgramId ?? defaultStakingProgramId;
186+
182187
// Create master safe if it doesn't exist
183188
try {
184189
if (!masterSafeAddress) {
@@ -197,9 +202,11 @@ const AgentNotRunningButton = () => {
197202
// Then create / deploy the service
198203
try {
199204
await ServicesService.createService({
200-
stakingProgramId: activeStakingProgramId ?? defaultStakingProgramId, // overwrite with StakingProgram.Alpha to test migration
205+
stakingProgramId,
201206
serviceTemplate,
202207
deploy: true,
208+
useMechMarketplace:
209+
stakingProgramId === StakingProgramId.BetaMechMarketplace,
203210
});
204211
} catch (error) {
205212
console.error(error);

frontend/components/MainPage/sections/RewardsSection/StakingRewardsThisEpoch.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Popover, Typography } from 'antd';
44
import { gql, request } from 'graphql-request';
55
import { z } from 'zod';
66

7-
import { SUBGRAPH_URL } from '@/constants/urls';
7+
import { GNOSIS_REWARDS_HISTORY_SUBGRAPH_URL } from '@/constants/urls';
88
import { POPOVER_WIDTH_MEDIUM } from '@/constants/width';
99
import { useStakingProgram } from '@/hooks/useStakingProgram';
1010
import { formatToTime } from '@/utils/time';
@@ -39,7 +39,10 @@ const useEpochEndTime = () => {
3939
const { data, isLoading } = useQuery({
4040
queryKey: ['latestEpochTime'],
4141
queryFn: async () => {
42-
const response = (await request(SUBGRAPH_URL, latestEpochTimeQuery)) as {
42+
const response = (await request(
43+
GNOSIS_REWARDS_HISTORY_SUBGRAPH_URL,
44+
latestEpochTimeQuery,
45+
)) as {
4346
checkpoints: EpochTimeResponse[];
4447
};
4548
return EpochTimeResponseSchema.parse(response.checkpoints[0]);

frontend/components/ManageStakingPage/StakingContractSection/CantMigrateAlert.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const AlertInsufficientMigrationFunds = ({
1919
}: CantMigrateAlertProps) => {
2020
const { serviceTemplate } = useServiceTemplates();
2121
const { isServiceStaked } = useStakingContractInfo();
22-
const { safeBalance, totalOlasStakedBalance } = useBalance();
22+
const { masterSafeBalance: safeBalance, totalOlasStakedBalance } =
23+
useBalance();
2324

2425
const totalOlasRequiredForStaking = getMinimumStakedAmountRequired(
2526
serviceTemplate,

frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx

+26-9
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,37 @@ type MigrateButtonProps = {
2323
export const MigrateButton = ({ stakingProgramId }: MigrateButtonProps) => {
2424
const { goto } = usePageState();
2525
const { serviceTemplate } = useServiceTemplates();
26-
const { setIsServicePollingPaused, setServiceStatus, updateServiceStatus } =
27-
useServices();
26+
const {
27+
setIsServicePollingPaused,
28+
setServiceStatus,
29+
updateServiceStatus,
30+
hasInitialLoaded: isServicesLoaded,
31+
service,
32+
} = useServices();
2833
const { setIsPaused: setIsBalancePollingPaused } = useBalance();
2934
const { updateActiveStakingProgramId: updateStakingProgram } =
3035
useStakingProgram();
3136
const { activeStakingContractInfo } = useStakingContractInfo();
3237
const { setMigrationModalOpen } = useModals();
3338

34-
const { migrateValidation } = useMigrate(stakingProgramId);
39+
const { migrateValidation, firstDeployValidation } =
40+
useMigrate(stakingProgramId);
41+
42+
// if false, user is migrating, not running for first time
43+
const isFirstDeploy = useMemo(() => {
44+
if (!isServicesLoaded) return false;
45+
if (service) return false;
46+
47+
return true;
48+
}, [isServicesLoaded, service]);
49+
50+
const validation = isFirstDeploy ? firstDeployValidation : migrateValidation;
3551

3652
const popoverContent = useMemo(() => {
37-
if (migrateValidation.canMigrate) return null;
53+
if (validation.canMigrate) return null;
3854

3955
if (
40-
migrateValidation.reason ===
41-
CantMigrateReason.NotStakedForMinimumDuration &&
56+
validation.reason === CantMigrateReason.NotStakedForMinimumDuration &&
4257
!isNil(activeStakingContractInfo)
4358
) {
4459
return (
@@ -48,15 +63,15 @@ export const MigrateButton = ({ stakingProgramId }: MigrateButtonProps) => {
4863
);
4964
}
5065

51-
return migrateValidation.reason;
52-
}, [activeStakingContractInfo, migrateValidation]);
66+
return validation.reason;
67+
}, [activeStakingContractInfo, validation]);
5368

5469
return (
5570
<Popover content={popoverContent}>
5671
<Button
5772
type="primary"
5873
size="large"
59-
disabled={!migrateValidation.canMigrate}
74+
disabled={!validation.canMigrate}
6075
onClick={async () => {
6176
setIsServicePollingPaused(true);
6277
setIsBalancePollingPaused(true);
@@ -69,6 +84,8 @@ export const MigrateButton = ({ stakingProgramId }: MigrateButtonProps) => {
6984
stakingProgramId,
7085
serviceTemplate,
7186
deploy: true,
87+
useMechMarketplace:
88+
stakingProgramId === StakingProgramId.BetaMechMarketplace,
7289
});
7390

7491
await updateStakingProgram();

frontend/components/ManageStakingPage/StakingContractSection/useMigrate.tsx

+82-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export enum CantMigrateReason {
2020
NoAvailableStakingSlots = 'The program has no more avaiable slots',
2121
NotStakedForMinimumDuration = 'Pearl has not been staked for the required minimum duration',
2222
PearlCurrentlyRunning = 'Unable to switch while Pearl is running',
23+
LoadingServices = 'Loading services...',
24+
CannotFindStakingContractInfo = 'Cannot obtain staking contract information',
2325
}
2426

2527
type MigrateValidation =
@@ -34,18 +36,26 @@ type MigrateValidation =
3436
export const useMigrate = (stakingProgramId: StakingProgramId) => {
3537
const { serviceStatus } = useServices();
3638
const { serviceTemplate } = useServiceTemplates();
37-
const { isBalanceLoaded, safeBalance, totalOlasStakedBalance } = useBalance();
39+
const {
40+
isBalanceLoaded,
41+
masterSafeBalance: safeBalance,
42+
totalOlasStakedBalance,
43+
} = useBalance();
3844
const { activeStakingProgramId, activeStakingProgramMeta } =
3945
useStakingProgram();
46+
4047
const {
4148
activeStakingContractInfo,
4249
isServiceStaked,
4350
isServiceStakedForMinimumDuration,
4451
isStakingContractInfoLoaded,
45-
isRewardsAvailable,
46-
hasEnoughServiceSlots,
52+
stakingContractInfoRecord,
4753
} = useStakingContractInfo();
4854

55+
const stakingContractInfo = stakingContractInfoRecord?.[stakingProgramId];
56+
57+
const { hasInitialLoaded: isServicesLoaded } = useServices();
58+
4959
const minimumOlasRequiredToMigrate = useMemo(
5060
() => getMinimumStakedAmountRequired(serviceTemplate, stakingProgramId),
5161
[serviceTemplate, stakingProgramId],
@@ -67,19 +77,37 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
6777
totalOlasStakedBalance,
6878
]);
6979

80+
const hasEnoughOlasForFirstRun = useMemo(() => {
81+
if (!isBalanceLoaded) return false;
82+
if (isNil(safeBalance?.OLAS)) return false;
83+
if (isNil(minimumOlasRequiredToMigrate)) return false;
84+
85+
return safeBalance.OLAS >= minimumOlasRequiredToMigrate;
86+
}, [isBalanceLoaded, minimumOlasRequiredToMigrate, safeBalance]);
87+
7088
const migrateValidation = useMemo<MigrateValidation>(() => {
71-
// loading requirements
89+
if (!isServicesLoaded) {
90+
return { canMigrate: false, reason: CantMigrateReason.LoadingServices };
91+
}
92+
7293
if (!isBalanceLoaded) {
7394
return { canMigrate: false, reason: CantMigrateReason.LoadingBalance };
7495
}
7596

76-
if (!isStakingContractInfoLoaded) {
97+
if (isServicesLoaded && !isStakingContractInfoLoaded) {
7798
return {
7899
canMigrate: false,
79100
reason: CantMigrateReason.LoadingStakingContractInfo,
80101
};
81102
}
82103

104+
if (!stakingContractInfo) {
105+
return {
106+
canMigrate: false,
107+
reason: CantMigrateReason.CannotFindStakingContractInfo,
108+
};
109+
}
110+
83111
// general requirements
84112
if (activeStakingProgramId === stakingProgramId) {
85113
return {
@@ -88,14 +116,17 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
88116
};
89117
}
90118

91-
if (!isRewardsAvailable) {
119+
if ((stakingContractInfo.availableRewards ?? 0) <= 0) {
92120
return {
93121
canMigrate: false,
94122
reason: CantMigrateReason.NoAvailableRewards,
95123
};
96124
}
97125

98-
if (!hasEnoughServiceSlots) {
126+
if (
127+
(stakingContractInfo.serviceIds ?? []).length >=
128+
(stakingContractInfo.maxNumServices ?? 0)
129+
) {
99130
return {
100131
canMigrate: false,
101132
reason: CantMigrateReason.NoAvailableStakingSlots,
@@ -145,12 +176,12 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
145176

146177
return { canMigrate: true };
147178
}, [
179+
isServicesLoaded,
148180
isBalanceLoaded,
149181
isStakingContractInfoLoaded,
182+
stakingContractInfo,
150183
activeStakingProgramId,
151184
stakingProgramId,
152-
isRewardsAvailable,
153-
hasEnoughServiceSlots,
154185
hasEnoughOlasToMigrate,
155186
isServiceStaked,
156187
activeStakingProgramMeta?.canMigrateTo,
@@ -159,7 +190,49 @@ export const useMigrate = (stakingProgramId: StakingProgramId) => {
159190
serviceStatus,
160191
]);
161192

193+
const firstDeployValidation = useMemo<MigrateValidation>(() => {
194+
if (!isServicesLoaded) {
195+
return { canMigrate: false, reason: CantMigrateReason.LoadingServices };
196+
}
197+
198+
if (!isBalanceLoaded) {
199+
return { canMigrate: false, reason: CantMigrateReason.LoadingBalance };
200+
}
201+
202+
if (!hasEnoughOlasForFirstRun) {
203+
return {
204+
canMigrate: false,
205+
reason: CantMigrateReason.InsufficientOlasToMigrate,
206+
};
207+
}
208+
209+
const stakingContractInfo = stakingContractInfoRecord?.[stakingProgramId];
210+
211+
if (stakingContractInfo?.availableRewards === 0) {
212+
return {
213+
canMigrate: false,
214+
reason: CantMigrateReason.NoAvailableRewards,
215+
};
216+
}
217+
218+
if (!stakingContractInfo?.maxNumServices) {
219+
return {
220+
canMigrate: false,
221+
reason: CantMigrateReason.NoAvailableStakingSlots,
222+
};
223+
}
224+
225+
return { canMigrate: true };
226+
}, [
227+
isServicesLoaded,
228+
isBalanceLoaded,
229+
hasEnoughOlasForFirstRun,
230+
stakingContractInfoRecord,
231+
stakingProgramId,
232+
]);
233+
162234
return {
163235
migrateValidation,
236+
firstDeployValidation,
164237
};
165238
};

0 commit comments

Comments
 (0)