diff --git a/apps/govern/components/Contract/Contract.spec.tsx b/apps/govern/components/Contract/Contract.spec.tsx
deleted file mode 100644
index 454dd2a0..00000000
--- a/apps/govern/components/Contract/Contract.spec.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import '@testing-library/jest-dom';
-import { render, screen } from '@testing-library/react';
-
-import { ContractPage } from './Contract';
-
-jest.mock('next/router', () => {
- return {
- useRouter: jest.fn().mockReturnValue({
- query: { address: '0x0000000000000000000000007248d855a3d4d17c32eb0d996a528f7520d2f4a3' },
- }),
- };
-});
-
-jest.mock('store/index', () => ({
- useAppSelector: jest.fn().mockReturnValue({
- isStakingContractsLoading: false,
- stakingContracts: [
- {
- address: '0x0000000000000000000000007248d855a3d4d17c32eb0d996a528f7520d2f4a3',
- chainId: 1,
- metadata: {
- name: 'Staking Contract Name 1',
- description: 'Some good contract description.',
- },
- },
- ],
- }),
-}));
-
-jest.mock('hooks/index', () => ({
- useContractParams: jest.fn().mockReturnValue({
- data: {
- implementation: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
- deployer: '0xdddddddddddddddddddddddddddddddddddddddd',
- isEnabled: true,
- },
- }),
-}));
-
-jest.mock('wagmi', () => ({
- useEnsName: jest.fn().mockReturnValue({ data: null }),
-}));
-
-describe('', () => {
- it('should display contract name and description', () => {
- render();
-
- expect(screen.getByText(/Staking Contract Name 1/)).toBeInTheDocument();
- expect(screen.getByText(/Some good contract description./)).toBeInTheDocument();
- });
-
- it('should display contract owner address', () => {
- render();
-
- expect(screen.getByText(/Owner address/)).toBeInTheDocument();
- expect(screen.getByText(/0xddddd...ddddd ↗/)).toBeInTheDocument();
- expect(screen.getByTestId('owner-address').getAttribute('href')).toBe(
- 'https://etherscan.io/address/0xdddddddddddddddddddddddddddddddddddddddd',
- );
- });
-
- it('should display contract chain name', () => {
- render();
-
- expect(screen.getByText(/Chain/)).toBeInTheDocument();
- expect(screen.getByText(/Ethereum/)).toBeInTheDocument();
- });
-
- it('should display contract address', () => {
- render();
-
- expect(screen.getByText(/Contract address/)).toBeInTheDocument();
- expect(screen.getByText(/0x7248d...2f4a3 ↗/)).toBeInTheDocument();
- expect(screen.getByTestId('contract-address').getAttribute('href')).toBe(
- 'https://etherscan.io/address/0x7248d855a3d4d17c32eb0d996a528f7520d2f4a3',
- );
- });
-});
diff --git a/apps/govern/components/Contract/Contract.tsx b/apps/govern/components/Contract/Contract.tsx
deleted file mode 100644
index 4085bd67..00000000
--- a/apps/govern/components/Contract/Contract.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { Alert, Card, Flex, Skeleton, Space, Typography } from 'antd';
-import { useRouter } from 'next/router';
-import styled from 'styled-components';
-import { StakingContract } from 'types';
-import { mainnet } from 'viem/chains';
-import { useEnsName } from 'wagmi';
-
-import { CHAIN_NAMES, EXPLORER_URLS, UNICODE_SYMBOLS } from 'libs/util-constants/src';
-
-import { getAddressFromBytes32, truncateAddress } from 'common-util/functions/addresses';
-import { useContractParams } from 'hooks/index';
-import { useAppSelector } from 'store/index';
-
-const StyledMain = styled.main`
- display: flex;
- flex-direction: column;
- max-width: 800px;
- margin: 0 auto;
-`;
-
-const Title = styled.h1`
- font-size: 24px;
- margin: 0 0 24px;
-`;
-
-const { Text, Paragraph } = Typography;
-
-type ContractPageContentProps = {
- contract: StakingContract;
-};
-
-const ContractPageContent = ({ contract }: ContractPageContentProps) => {
- const formattedAddress = contract ? getAddressFromBytes32(contract.address) : '';
-
- const { data: contractParams } = useContractParams(formattedAddress, contract.chainId);
- const { data: ensName, isFetching: isEnsNameFetching } = useEnsName({
- address: contractParams?.deployer,
- chainId: mainnet.id,
- query: { refetchOnWindowFocus: false },
- });
-
- return (
- <>
-
{contract.metadata.name}
-
- Description
- {contract.metadata.description}
-
-
-
-
- Owner address
- {contractParams && !isEnsNameFetching ? (
-
- {`${ensName || truncateAddress(contractParams.deployer)} ${
- UNICODE_SYMBOLS.EXTERNAL_LINK
- }`}
-
- ) : (
-
- )}
-
-
-
- Chain
- {CHAIN_NAMES[contract.chainId]}
-
-
-
- Contract address
-
- {`${truncateAddress(formattedAddress)} ${UNICODE_SYMBOLS.EXTERNAL_LINK}`}
-
-
-
- >
- );
-};
-
-const ContractContent = () => {
- const router = useRouter();
- const { isStakingContractsLoading, stakingContracts } = useAppSelector((state) => state.govern);
-
- const contract = stakingContracts.find((item) => item.address === router?.query?.address);
-
- if (isStakingContractsLoading) {
- return ;
- }
-
- if (!contract) {
- return (
-
- );
- }
-
- return ;
-};
-
-export const ContractPage = () => (
-
-
-
-
-
-);
diff --git a/apps/govern/components/Contract/ContractConfiguration.tsx b/apps/govern/components/Contract/ContractConfiguration.tsx
new file mode 100644
index 00000000..b9ddd252
--- /dev/null
+++ b/apps/govern/components/Contract/ContractConfiguration.tsx
@@ -0,0 +1,291 @@
+import { Col, Flex, Row, Skeleton, Typography } from 'antd';
+import { ReactNode, useMemo } from 'react';
+import { StakingContract } from 'types';
+import { Address, zeroHash } from 'viem';
+
+import { GATEWAY_URL, NA } from '@autonolas/frontend-library';
+
+import { EXPLORER_URLS, HASH_PREFIX, REGISTRY_URL, UNICODE_SYMBOLS } from 'libs/util-constants/src';
+import { truncateAddress } from 'libs/util-functions/src';
+
+import {
+ ActivityCheckerAddressLabel,
+ AgentIdsLabel,
+ AgentInstancesLabel,
+ LivenessPeriodLabel,
+ MaximumInactivityPeriodsLabel,
+ MaximumStakedAgentsLabel,
+ MinimumStakingDepositLabel,
+ MinimumStakingPeriodsLabel,
+ MultisigThresholdLabel,
+ RewardsPerSecondLabel,
+ ServiceConfigHashLabel,
+ TimeForEmissionsLabel,
+} from './FieldLabels';
+import {
+ useGetActivityChecker,
+ useGetAgentIds,
+ useGetConfigHash,
+ useGetLivenessPeriod,
+ useGetMaximumInactivityPeriods,
+ useGetMinimumStakingDeposit,
+ useGetMinimumStakingDuration,
+ useGetMultisigThreshold,
+ useMaxNumServices,
+ useNumberOfAgentInstances,
+ useRewardsPerSecond,
+ useTimeForEmissions,
+} from './hooks';
+
+const { Text } = Typography;
+
+const ShowContent = ({
+ isLoading,
+ chainId,
+ data,
+}: {
+ isLoading: boolean;
+ chainId: number;
+ data?: string | ReactNode;
+}) => {
+ if (!chainId || isLoading) return ;
+ return {data || NA};
+};
+
+type ConfigItemProps = {
+ address: Address;
+ chainId: number;
+};
+
+const ShowNetworkAddress = ({ address, chainId }: ConfigItemProps) => {
+ if (!chainId) return null;
+ if (!address) return null;
+
+ const truncatedAddress = truncateAddress(address);
+ return (
+
+ {`${truncatedAddress} ${UNICODE_SYMBOLS.EXTERNAL_LINK}`}
+
+ );
+};
+
+const MaximumStakedAgents = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useMaxNumServices({ address, chainId });
+ return ;
+};
+
+const Rewards = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useRewardsPerSecond({ address, chainId });
+ return ;
+};
+
+const MinimumStakingDeposit = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useGetMinimumStakingDeposit({ address, chainId });
+ return ;
+};
+
+const MinimumStakingPeriods = ({ address, chainId }: ConfigItemProps) => {
+ const { isLoading: isLivenessPeriodLoading, data: livenessPeriod } = useGetLivenessPeriod({
+ address,
+ chainId,
+ });
+ const { isLoading: isMinimumStakingPeriodsLoading, data: minimumStakingDuration } =
+ useGetMinimumStakingDuration({ address, chainId });
+
+ const isLoading = useMemo(
+ () => isLivenessPeriodLoading || isMinimumStakingPeriodsLoading,
+ [isLivenessPeriodLoading, isMinimumStakingPeriodsLoading],
+ );
+ const data = useMemo(() => {
+ if (!livenessPeriod) return NA;
+ if (!minimumStakingDuration) return NA;
+
+ const minimumStakingPeriods = Number(minimumStakingDuration) / Number(livenessPeriod);
+ return minimumStakingPeriods;
+ }, [livenessPeriod, minimumStakingDuration]);
+
+ return ;
+};
+
+const MaximumInactivityPeriods = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useGetMaximumInactivityPeriods({ address, chainId });
+ return ;
+};
+
+const LivenessPeriod = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useGetLivenessPeriod({ address, chainId });
+ return ;
+};
+
+const TimeForEmissions = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useTimeForEmissions({ address, chainId });
+ return ;
+};
+
+const NumAgentInstances = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useNumberOfAgentInstances({ address, chainId });
+ return ;
+};
+
+const AgentIds = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useGetAgentIds({ address, chainId });
+ const networkName = 'gnosis';
+
+ const ids = useMemo(() => {
+ if (!data || data.length === 0) return NA;
+ return data.map((id) => (
+
+ {id}
+
+ ));
+ }, [data, networkName]);
+
+ return ;
+};
+
+const MultisigThreshold = ({ address, chainId }: ConfigItemProps) => {
+ const { data, isLoading } = useGetMultisigThreshold({ address, chainId });
+ return ;
+};
+
+const ConfigHash = ({ address, chainId }: ConfigItemProps) => {
+ const { data: configHash, isLoading } = useGetConfigHash({ address, chainId });
+ const isZeroAddress = configHash === zeroHash;
+
+ const calculatedConfigHash = useMemo(() => {
+ if (!configHash) return NA;
+
+ const truncateConfigHash = truncateAddress(configHash);
+
+ // if configHash is zero address, no need to show external link
+ if (isZeroAddress) return truncateConfigHash;
+
+ const uri = `${HASH_PREFIX}${configHash.substring(2)}`;
+ const ipfsUrl = `${GATEWAY_URL}${uri}`;
+ return (
+
+ {truncateConfigHash} {UNICODE_SYMBOLS.EXTERNAL_LINK}
+
+ );
+ }, [configHash, isZeroAddress]);
+
+ return ;
+};
+
+const ActivityCheckerAddress = ({ address, chainId }: ConfigItemProps) => {
+ const { data: checkerAddress, isLoading } = useGetActivityChecker({ address, chainId });
+ return (
+ }
+ />
+ );
+};
+
+type ColFlexContainerProps = { text: string | ReactNode; content: ReactNode };
+
+export const ColFlexContainer = ({ text, content, ...rest }: ColFlexContainerProps) => {
+ return (
+
+
+ {typeof text === 'string' ? {text} : text}
+ {content}
+
+
+ );
+};
+
+/**
+ * contract configuration details component for details page
+ */
+export const ContractConfiguration = ({ contract }: { contract: StakingContract }) => {
+ return (
+ <>
+
+ }
+ content={}
+ data-testid="maximum-staked-agents"
+ />
+ }
+ content={}
+ data-testid="rewards-per-second"
+ />
+
+
+
+ }
+ content={}
+ data-testid="minimum-staking-deposit"
+ />
+ }
+ content={}
+ data-testid="minimum-staking-periods"
+ />
+
+
+
+ }
+ content={
+
+ }
+ data-testid="maximum-inactivity-periods"
+ />
+ }
+ content={}
+ data-testid="liveness-period"
+ />
+
+
+
+ }
+ content={}
+ data-testid="time-for-emissions"
+ />
+ }
+ content={}
+ data-testid="num-agent-instances"
+ />
+
+
+
+ }
+ content={}
+ data-testid="agent-ids"
+ />
+ }
+ content={}
+ data-testid="multisig-threshold"
+ />
+
+
+
+ }
+ content={}
+ data-testid="service-config-hash"
+ />
+ }
+ content={}
+ data-testid="activity-checker-address"
+ />
+
+ >
+ );
+};
diff --git a/apps/govern/components/Contract/FieldLabels.tsx b/apps/govern/components/Contract/FieldLabels.tsx
new file mode 100644
index 00000000..a8a969d8
--- /dev/null
+++ b/apps/govern/components/Contract/FieldLabels.tsx
@@ -0,0 +1,204 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Flex, Tooltip, Typography } from 'antd';
+import { ReactNode } from 'react';
+
+import { COLOR } from 'libs/ui-theme/src';
+import { UNICODE_SYMBOLS } from 'libs/util-constants/src';
+
+const { Paragraph, Text } = Typography;
+
+/**
+ * List of FieldConfig for staking contract with name and description for each field
+ */
+export const FieldConfig = {
+ contractName: { name: 'Name', desc: null },
+ description: { name: 'Description', desc: null },
+ maxNumServices: {
+ name: 'Maximum number of staked agents',
+ desc: (
+ <>
+ How many agents do you need running? Agents can be sovereign or decentralized agents. They
+ join the contract on a first come, first serve basis.
+
+
+ Learn more {UNICODE_SYMBOLS.EXTERNAL_LINK}
+
+ >
+ ),
+ },
+ rewardsPerSecond: {
+ name: 'Rewards, OLAS per second',
+ desc: 'Token rewards come from the Olas protocol',
+ },
+ minStakingDeposit: {
+ name: 'Minimum service staking deposit, OLAS',
+ desc: (
+
+
+ The established value of minimal non-slashable security deposit and minimal slashable
+ operator bonds required for staking.
+
+
+
+ Operators need to stake:
+ this × the number of agent instances + 1.
+
+
+ ),
+ },
+ minNumStakingPeriods: {
+ name: 'Minimum number of staking periods',
+ desc: 'Minimum number of staking periods before the service can be unstaked',
+ },
+ maxNumInactivityPeriods: {
+ name: 'Maximum number of inactivity periods',
+ desc: 'Maximum duration of inactivity permitted for the agent before facing eviction.',
+ },
+ livenessPeriod: {
+ name: 'Liveness period, seconds',
+ desc: (
+
+
+ Time frame in seconds during which the staking contract assesses the activity of the
+ service.
+
+
+ 24 hours - 86400 seconds
+
+ ),
+ },
+ timeForEmissions: {
+ name: 'Time for emissions, seconds',
+ desc: (
+
+
+ Time for which staking emissions are requested in order to feed a staking contract
+ considering that all the service slots are filled and all services are active.
+
+
+ 30 days - 2592000 seconds
+
+ ),
+ },
+ numAgentInstances: {
+ name: 'Number of agent instances',
+ desc: 'Quantity of agent instances associated with an autonomous service registered in the staking contract.',
+ },
+ agentIds: {
+ name: 'Agent IDs',
+ desc: 'If set, serves as a requirement for a service to be comprised of agent Ids specified.',
+ },
+ threshold: {
+ name: 'Multisig threshold',
+ desc: 'Service multisig threshold requirement. 0 - no threshold is enforced',
+ },
+ configHash: {
+ name: 'Service configuration hash',
+ desc: 'Service configuration hash requirement',
+ },
+ activityChecker: {
+ name: 'Activity checker address',
+ desc: 'Activity checker handles the logic to monitor whether a specific service activity has been performed.',
+ },
+} as const;
+
+const TextWithTooltip = ({
+ text,
+ description,
+}: {
+ text: string;
+ description?: string | ReactNode;
+}) => {
+ if (!description) return {text};
+
+ return (
+ {description}}>
+
+ {text}
+
+
+ );
+};
+
+export const NameLabel = () => ;
+
+export const DescriptionLabel = () => ;
+
+export const MaximumStakedAgentsLabel = () => (
+
+);
+
+export const RewardsPerSecondLabel = () => (
+
+);
+
+export const TemplateInfo = () => (
+
+);
+
+export const MinimumStakingDepositLabel = () => (
+
+);
+
+export const MinimumStakingPeriodsLabel = () => (
+
+);
+
+export const MaximumInactivityPeriodsLabel = () => (
+
+);
+
+export const LivenessPeriodLabel = () => (
+
+);
+
+export const TimeForEmissionsLabel = () => (
+
+);
+
+export const AgentInstancesLabel = () => (
+
+);
+
+export const AgentIdsLabel = () => (
+
+);
+
+export const MultisigThresholdLabel = () => (
+
+);
+
+export const ServiceConfigHashLabel = () => (
+
+);
+
+export const ActivityCheckerAddressLabel = () => (
+
+);
diff --git a/apps/govern/components/Contract/hooks.ts b/apps/govern/components/Contract/hooks.ts
new file mode 100644
index 00000000..8c4cd7d2
--- /dev/null
+++ b/apps/govern/components/Contract/hooks.ts
@@ -0,0 +1,122 @@
+import { Address, formatEther } from 'viem';
+import { useReadContract } from 'wagmi';
+
+import { STAKING_FACTORY, STAKING_TOKEN } from 'libs/util-contracts/src/lib/abiAndAddresses';
+
+export const useContractParams = (address: string, chainId: number) => {
+ const { data } = useReadContract({
+ address: (STAKING_FACTORY.addresses as Record)[chainId],
+ abi: STAKING_FACTORY.abi,
+ chainId,
+ functionName: 'mapInstanceParams',
+ args: [address],
+ query: {
+ select: (data) => {
+ const [implementation, deployer, isEnabled] = data as [Address, Address, boolean];
+ return { implementation, deployer, isEnabled };
+ },
+ },
+ });
+
+ return { data };
+};
+
+type UseStakingContractConstantParams = {
+ address: Address;
+ chainId: number;
+};
+
+const useStakingContractConstant = ({
+ name,
+ address,
+ chainId,
+ select,
+}: {
+ address: Address;
+ chainId: number;
+ name: string;
+ select?: (data: unknown) => T;
+}) => {
+ return useReadContract({
+ address,
+ abi: STAKING_TOKEN.abi,
+ chainId,
+ functionName: name,
+ query: {
+ enabled: !!chainId && !!address && !!name,
+ select:
+ select ||
+ (((data) => {
+ if (typeof data === 'bigint') return data.toString();
+ return data || '0';
+ }) as (data: unknown) => T),
+ },
+ });
+};
+
+export const useMaxNumServices = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({
+ address,
+ chainId,
+ name: 'maxNumServices',
+ });
+
+export const useRewardsPerSecond = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({
+ address,
+ chainId,
+ name: 'rewardsPerSecond',
+ select: (data) => (typeof data === 'bigint' ? `${Number(formatEther(data))}` : '0'),
+ });
+
+export const useGetMinimumStakingDeposit = ({
+ address,
+ chainId,
+}: UseStakingContractConstantParams) =>
+ useStakingContractConstant({
+ address,
+ chainId,
+ name: 'minStakingDeposit',
+ select: (data) => (typeof data === 'bigint' ? `${Number(formatEther(data))}` : '0'),
+ });
+
+export const useGetMinimumStakingDuration = ({
+ address,
+ chainId,
+}: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'minStakingDuration' });
+
+export const useGetMaximumInactivityPeriods = ({
+ address,
+ chainId,
+}: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'maxNumInactivityPeriods' });
+
+export const useGetLivenessPeriod = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'livenessPeriod' });
+
+export const useTimeForEmissions = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'timeForEmissions' });
+
+export const useNumberOfAgentInstances = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'numAgentInstances' });
+
+export const useGetAgentIds = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({
+ address,
+ chainId,
+ name: 'getAgentIds',
+ select: (data) => {
+ const ids = data as bigint[];
+ return ids.map((id) => id.toString());
+ },
+ });
+
+export const useGetMultisigThreshold = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'threshold' });
+
+export const useGetConfigHash = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'configHash' });
+
+export const useGetActivityChecker = ({ address, chainId }: UseStakingContractConstantParams) =>
+ useStakingContractConstant({ address, chainId, name: 'activityChecker' });
diff --git a/apps/govern/components/Contract/index.spec.tsx b/apps/govern/components/Contract/index.spec.tsx
new file mode 100644
index 00000000..80b27443
--- /dev/null
+++ b/apps/govern/components/Contract/index.spec.tsx
@@ -0,0 +1,163 @@
+import '@testing-library/jest-dom';
+import { render, screen, within } from '@testing-library/react';
+
+import { ContractPage } from './index';
+
+jest.mock('next/router', () => {
+ return {
+ useRouter: jest.fn().mockReturnValue({
+ query: { address: '0x0000000000000000000000007248d855a3d4d17c32eb0d996a528f7520d2f4a3' },
+ }),
+ };
+});
+
+jest.mock('store/index', () => ({
+ useAppSelector: jest.fn().mockReturnValue({
+ isStakingContractsLoading: false,
+ stakingContracts: [
+ {
+ address: '0x0000000000000000000000007248d855a3d4d17c32eb0d996a528f7520d2f4a3',
+ chainId: 1,
+ metadata: {
+ name: 'Staking Contract Name 1',
+ description: 'Some good contract description.',
+ },
+ },
+ ],
+ }),
+}));
+
+jest.mock('components/Contract/hooks', () => ({
+ useContractParams: jest.fn().mockReturnValue({
+ data: {
+ implementation: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ deployer: '0xdddddddddddddddddddddddddddddddddddddddd',
+ isEnabled: true,
+ },
+ }),
+ useGetActivityChecker: jest
+ .fn()
+ .mockReturnValue({ data: '0x0000000000000000000000000000000000000000' }),
+ useGetAgentIds: jest.fn().mockReturnValue({ data: ['25'] }),
+ useGetConfigHash: jest.fn().mockReturnValue({
+ data: '0x0000000000000000000000000000000000000000000000000000000000000000',
+ }),
+ useGetLivenessPeriod: jest.fn().mockReturnValue({ data: '86400' }),
+ useGetMaximumInactivityPeriods: jest.fn().mockReturnValue({ data: '2' }),
+ useGetMinimumStakingDuration: jest.fn().mockReturnValue({ data: '259200' }),
+ useTimeForEmissions: jest.fn().mockReturnValue({ data: '2592000' }),
+ useNumberOfAgentInstances: jest.fn().mockReturnValue({ data: '1' }),
+ useGetMultisigThreshold: jest.fn().mockReturnValue({ data: '1' }),
+ useGetMinimumStakingDeposit: jest.fn().mockReturnValue({ data: '20' }),
+ useMaxNumServices: jest.fn().mockReturnValue({ data: 200 }),
+ useRewardsPerSecond: jest.fn().mockReturnValue({ data: 0.003 }),
+}));
+
+jest.mock('wagmi', () => ({
+ useEnsName: jest.fn().mockReturnValue({ data: null }),
+}));
+
+describe('', () => {
+ it('should display contract name and description', () => {
+ render();
+
+ expect(screen.getByText(/Staking Contract Name 1/)).toBeInTheDocument();
+ expect(screen.getByText(/Some good contract description./)).toBeInTheDocument();
+ });
+
+ it('should display contract owner address', () => {
+ render();
+
+ expect(screen.getByText(/Owner address/)).toBeInTheDocument();
+ expect(screen.getByText(/0xddddd...ddddd ↗/)).toBeInTheDocument();
+ expect(screen.getByTestId('owner-address').getAttribute('href')).toBe(
+ 'https://etherscan.io/address/0xdddddddddddddddddddddddddddddddddddddddd',
+ );
+ });
+
+ it('should display contract chain name', () => {
+ render();
+
+ expect(screen.getByText(/Chain/)).toBeInTheDocument();
+ expect(screen.getByText(/Ethereum/)).toBeInTheDocument();
+ });
+
+ it('should display contract address', () => {
+ render();
+
+ expect(screen.getByText(/Contract address/)).toBeInTheDocument();
+ expect(screen.getByText(/0x7248d...2f4a3 ↗/)).toBeInTheDocument();
+ expect(screen.getByTestId('contract-address').getAttribute('href')).toBe(
+ 'https://etherscan.io/address/0x7248d855a3d4d17c32eb0d996a528f7520d2f4a3',
+ );
+ });
+
+ it.each([
+ {
+ testId: 'maximum-staked-agents',
+ title: 'Maximum number of staked agents',
+ value: '200',
+ },
+ {
+ testId: 'rewards-per-second',
+ title: 'Rewards, OLAS per second',
+ value: '0.003',
+ },
+ {
+ testId: 'minimum-staking-deposit',
+ title: 'Minimum service staking deposit, OLAS',
+ value: '20',
+ },
+ {
+ testId: 'minimum-staking-periods',
+ title: 'Minimum number of staking periods',
+ value: '3',
+ },
+ {
+ testId: 'maximum-inactivity-periods',
+ title: 'Maximum number of inactivity periods',
+ value: '2',
+ },
+ {
+ testId: 'liveness-period',
+ title: 'Liveness period, seconds',
+ value: '86400',
+ },
+ {
+ testId: 'time-for-emissions',
+ title: 'Time for emissions, seconds',
+ value: '2592000',
+ },
+ {
+ testId: 'num-agent-instances',
+ title: 'Number of agent instances',
+ value: '1',
+ },
+ {
+ testId: 'agent-ids',
+ title: 'Agent IDs',
+ value: '25',
+ },
+ {
+ testId: 'multisig-threshold',
+ title: 'Multisig threshold',
+ value: '1',
+ },
+ {
+ testId: 'service-config-hash',
+ title: 'Service configuration hash',
+ value: '0x00000...00000',
+ },
+ {
+ testId: 'activity-checker-address',
+ title: 'Activity checker address',
+ value: '0x00000...00000 ↗',
+ },
+ ])('should display $title', async ({ testId, title, value }) => {
+ render();
+ const section = await screen.findByTestId(testId);
+
+ expect(within(section).getByText(title)).toBeInTheDocument();
+ expect(within(section).getByText(value)).toBeInTheDocument();
+ });
+});
diff --git a/apps/govern/components/Contract/index.tsx b/apps/govern/components/Contract/index.tsx
new file mode 100644
index 00000000..33ef5800
--- /dev/null
+++ b/apps/govern/components/Contract/index.tsx
@@ -0,0 +1,136 @@
+import { Alert, Card, Flex, Skeleton, Space, Typography } from 'antd';
+import { useRouter } from 'next/router';
+import styled from 'styled-components';
+import { StakingContract } from 'types';
+import { mainnet } from 'viem/chains';
+import { useEnsName } from 'wagmi';
+
+import { CHAIN_NAMES, EXPLORER_URLS, UNICODE_SYMBOLS } from 'libs/util-constants/src';
+
+import { getAddressFromBytes32, truncateAddress } from 'common-util/functions/addresses';
+import { useAppSelector } from 'store/index';
+
+import { ContractConfiguration } from './ContractConfiguration';
+import { useContractParams } from './hooks';
+
+const StyledMain = styled.main`
+ display: flex;
+ flex-direction: column;
+ max-width: 800px;
+ margin: 0 auto;
+`;
+
+const Title = styled.h1`
+ font-size: 24px;
+ margin: 0 0 24px;
+`;
+
+const { Text, Paragraph } = Typography;
+const AntdTitle = Typography.Title;
+
+type ContractPageContentProps = {
+ contract: StakingContract;
+};
+
+const ContractPageContent = ({ contract }: ContractPageContentProps) => {
+ const formattedAddress = getAddressFromBytes32(contract.address);
+
+ const { data: contractParams } = useContractParams(formattedAddress, contract.chainId);
+ const { data: ensName, isFetching: isEnsNameFetching } = useEnsName({
+ address: contractParams?.deployer,
+ chainId: mainnet.id,
+ query: { refetchOnWindowFocus: false },
+ });
+
+ return (
+ <>
+
+ {contract.metadata.name}
+
+ Description
+ {contract.metadata.description}
+
+
+
+
+ Owner address
+ {contractParams && !isEnsNameFetching ? (
+
+ {`${ensName || truncateAddress(contractParams.deployer)} ${
+ UNICODE_SYMBOLS.EXTERNAL_LINK
+ }`}
+
+ ) : (
+
+ )}
+
+
+
+ Chain
+ {CHAIN_NAMES[contract.chainId]}
+
+
+
+ Contract address
+
+ {`${truncateAddress(formattedAddress)} ${UNICODE_SYMBOLS.EXTERNAL_LINK}`}
+
+
+
+
+
+
+
+
+ Contract configuration
+
+
+
+
+ >
+ );
+};
+
+const SkeletonPage = () => (
+
+
+
+
+
+);
+
+export const ContractPage = () => {
+ const router = useRouter();
+ const { isStakingContractsLoading, stakingContracts } = useAppSelector((state) => state.govern);
+
+ const contract = stakingContracts.find((item) => item.address === router?.query?.address);
+
+ if (isStakingContractsLoading) {
+ return ;
+ }
+
+ if (!contract) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/govern/hooks/index.ts b/apps/govern/hooks/index.ts
index 0076b892..b14e9d42 100644
--- a/apps/govern/hooks/index.ts
+++ b/apps/govern/hooks/index.ts
@@ -2,4 +2,3 @@ export * from './useFetchStakingContractsList';
export * from './useFetchUserVotes';
export * from './useFetchBalances';
export * from './useVotingPower';
-export * from './useContractParams';
diff --git a/apps/govern/hooks/useContractParams.ts b/apps/govern/hooks/useContractParams.ts
deleted file mode 100644
index 59a00e77..00000000
--- a/apps/govern/hooks/useContractParams.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Address } from 'viem';
-import { useReadContract } from 'wagmi';
-
-import { STAKING_FACTORY } from 'libs/util-contracts/src/lib/abiAndAddresses';
-
-export const useContractParams = (address: string, chainId: number) => {
- const { data } = useReadContract({
- address: (STAKING_FACTORY.addresses as Record)[chainId],
- abi: STAKING_FACTORY.abi,
- chainId,
- functionName: 'mapInstanceParams',
- args: [address],
- query: {
- select: (data) => {
- const [implementation, deployer, isEnabled] = data as [Address, Address, boolean];
- return { implementation, deployer, isEnabled };
- },
- },
- });
-
- return { data };
-};
diff --git a/apps/govern/pages/contracts/[address].tsx b/apps/govern/pages/contracts/[address].tsx
index d00bc0fb..5d733b74 100644
--- a/apps/govern/pages/contracts/[address].tsx
+++ b/apps/govern/pages/contracts/[address].tsx
@@ -1,3 +1,3 @@
-import { ContractPage } from 'components/Contract/Contract';
+import { ContractPage } from 'components/Contract';
export default ContractPage;
diff --git a/apps/launch/components/MyStakingContracts/Create/index.spec.tsx b/apps/launch/components/MyStakingContracts/Create/index.spec.tsx
index 6bbe2f5e..cb27a93c 100644
--- a/apps/launch/components/MyStakingContracts/Create/index.spec.tsx
+++ b/apps/launch/components/MyStakingContracts/Create/index.spec.tsx
@@ -168,7 +168,7 @@ describe('', () => {
it('should display `Liveness period` field as required and able to fill the field', async () => {
render();
- const livenessPeriodInput = screen.getByLabelText('Liveness period (sec)');
+ const livenessPeriodInput = screen.getByLabelText('Liveness period, seconds');
expect(livenessPeriodInput).toBeRequired();
expect(livenessPeriodInput).toHaveValue('86400'); // default value
@@ -178,7 +178,7 @@ describe('', () => {
it('should display `Time for emissions` field as required, able to fill and see the error message when the value is out of range', async () => {
render();
- const timeForEmissionsInput = screen.getByLabelText('Time for emissions (sec)');
+ const timeForEmissionsInput = screen.getByLabelText('Time for emissions, seconds');
expect(timeForEmissionsInput).toBeRequired();
expect(timeForEmissionsInput).toHaveValue('2592000'); // default value
diff --git a/apps/launch/components/MyStakingContracts/Details/ContractConfiguration.tsx b/apps/launch/components/MyStakingContracts/Details/ContractConfiguration.tsx
index 38bb1f38..0569cdda 100644
--- a/apps/launch/components/MyStakingContracts/Details/ContractConfiguration.tsx
+++ b/apps/launch/components/MyStakingContracts/Details/ContractConfiguration.tsx
@@ -1,4 +1,4 @@
-import { Row, Skeleton, Typography } from 'antd';
+import { Col, Flex, Row, Skeleton, Typography } from 'antd';
import { FC, ReactNode, useMemo } from 'react';
import { Address } from 'viem';
@@ -17,7 +17,9 @@ import {
useGetMinimumStakingDeposit,
useGetMinimumStakingDuration,
useGetMultisigThreshold,
+ useMaxNumServices,
useNumberOfAgentInstances,
+ useRewardsPerSecond,
useTimeForEmissions,
} from 'hooks/useGetStakingConstants';
import { useAppSelector } from 'store/index';
@@ -37,8 +39,6 @@ import {
ServiceConfigHashLabel,
TimeForEmissionsLabel,
} from '../FieldLabels';
-import { ColFlexContainer } from './helpers';
-import { useMaxNumServices, useRewardsPerSecond } from './hooks';
const { Text } = Typography;
@@ -70,7 +70,7 @@ const MaximumStakedAgents: FC<{ address: Address }> = ({ address }) => {
const Rewards: FC<{ address: Address }> = ({ address }) => {
const { data, isLoading } = useRewardsPerSecond({ address });
- return ;
+ return ;
};
const MinimumStakingDeposit: FC<{ address: Address }> = ({ address }) => {
@@ -107,7 +107,7 @@ const MaximumInactivityPeriods: FC<{ address: Address }> = ({ address }) => {
const LivenessPeriod: FC<{ address: Address }> = ({ address }) => {
const { data, isLoading } = useGetLivenessPeriod({ address });
- return ;
+ return ;
};
const TimeForEmissions: FC<{ address: Address }> = ({ address }) => {
@@ -180,6 +180,19 @@ const ActivityCheckerAddress: FC<{ address: Address }> = ({ address }) => {
);
};
+type ColFlexContainerProps = { text: string | ReactNode; content: ReactNode };
+
+export const ColFlexContainer = ({ text, content, ...rest }: ColFlexContainerProps) => {
+ return (
+
+
+ {typeof text === 'string' ? {text} : text}
+ {content}
+
+
+ );
+};
+
/**
* contract configuration details component for details page
*/
diff --git a/apps/launch/components/MyStakingContracts/Details/helpers.tsx b/apps/launch/components/MyStakingContracts/Details/helpers.tsx
deleted file mode 100644
index 81feeec4..00000000
--- a/apps/launch/components/MyStakingContracts/Details/helpers.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Col, Flex, Typography } from 'antd';
-import { ReactNode } from 'react';
-
-const { Text } = Typography;
-
-type ColFlexContainerProps = { text: string | ReactNode; content: ReactNode };
-
-export const ColFlexContainer = ({ text, content, ...rest }: ColFlexContainerProps) => {
- return (
-
-
- {typeof text === 'string' ? {text} : text}
- {content}
-
-
- );
-};
diff --git a/apps/launch/components/MyStakingContracts/Details/hooks.tsx b/apps/launch/components/MyStakingContracts/Details/hooks.tsx
deleted file mode 100644
index d3c338cf..00000000
--- a/apps/launch/components/MyStakingContracts/Details/hooks.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { ethers } from 'ethers';
-import { Address } from 'viem';
-import { useReadContract } from 'wagmi';
-
-import { STAKING_TOKEN } from 'libs/util-contracts/src/lib/abiAndAddresses';
-
-import { useAppSelector } from 'store/index';
-
-export const useMaxNumServices = ({ address }: { address: Address }) => {
- const { networkId } = useAppSelector((state) => state.network);
-
- return useReadContract({
- address,
- abi: STAKING_TOKEN.abi,
- chainId: networkId as number,
- functionName: 'maxNumServices',
- query: {
- enabled: !!networkId,
- select: (data) => (typeof data === 'bigint' ? data.toString() : '0'),
- },
- });
-};
-
-export const useRewardsPerSecond = ({ address }: { address: Address }) => {
- const { networkId } = useAppSelector((state) => state.network);
-
- return useReadContract({
- address,
- abi: STAKING_TOKEN.abi,
- chainId: networkId as number,
- functionName: 'rewardsPerSecond',
- query: {
- enabled: !!networkId,
- select: (data) => (typeof data === 'bigint' ? ethers.formatEther(data) : '0'),
- },
- });
-};
diff --git a/apps/launch/components/MyStakingContracts/Details/index.spec.tsx b/apps/launch/components/MyStakingContracts/Details/index.spec.tsx
index 0c9a9316..fde694fa 100644
--- a/apps/launch/components/MyStakingContracts/Details/index.spec.tsx
+++ b/apps/launch/components/MyStakingContracts/Details/index.spec.tsx
@@ -43,9 +43,6 @@ jest.mock('hooks/useGetStakingConstants', () => ({
useNumberOfAgentInstances: jest.fn().mockReturnValue({ data: '1' }),
useGetMultisigThreshold: jest.fn().mockReturnValue({ data: '1' }),
useGetMinimumStakingDeposit: jest.fn().mockReturnValue({ data: '20' }),
-}));
-
-jest.mock('./hooks', () => ({
useMaxNumServices: jest.fn().mockReturnValue({ data: 200 }),
useRewardsPerSecond: jest.fn().mockReturnValue({ data: 0.003 }),
}));
@@ -117,7 +114,7 @@ describe(' ', () => {
{
testId: 'rewards-per-second',
title: 'Rewards, OLAS per second',
- value: '0.003 OLAS',
+ value: '0.003',
},
{
testId: 'minimum-staking-deposit',
@@ -136,12 +133,12 @@ describe(' ', () => {
},
{
testId: 'liveness-period',
- title: 'Liveness period (sec)',
- value: '86400 seconds',
+ title: 'Liveness period, seconds',
+ value: '86400',
},
{
testId: 'time-for-emissions',
- title: 'Time for emissions (sec)',
+ title: 'Time for emissions, seconds',
value: '2592000',
},
{
diff --git a/apps/launch/components/MyStakingContracts/Details/index.tsx b/apps/launch/components/MyStakingContracts/Details/index.tsx
index 74316f47..0a585454 100644
--- a/apps/launch/components/MyStakingContracts/Details/index.tsx
+++ b/apps/launch/components/MyStakingContracts/Details/index.tsx
@@ -12,8 +12,7 @@ import { URL } from 'common-util/constants/urls';
import { useAppSelector } from 'store/index';
import { MyStakingContract } from 'types/index';
-import { ContractConfiguration } from './ContractConfiguration';
-import { ColFlexContainer } from './helpers';
+import { ColFlexContainer, ContractConfiguration } from './ContractConfiguration';
const { Paragraph, Text, Title } = Typography;
diff --git a/apps/launch/components/MyStakingContracts/FieldConfig.tsx b/apps/launch/components/MyStakingContracts/FieldConfig.tsx
index 2d325f22..b9678d87 100644
--- a/apps/launch/components/MyStakingContracts/FieldConfig.tsx
+++ b/apps/launch/components/MyStakingContracts/FieldConfig.tsx
@@ -55,7 +55,7 @@ export const FieldConfig: Record
- Operators need to stake:
+ Operators need to stake:
this × the number of agent instances + 1.
@@ -70,7 +70,7 @@ export const FieldConfig: Record
@@ -83,7 +83,7 @@ export const FieldConfig: Record
diff --git a/apps/launch/hooks/useGetStakingConstants.ts b/apps/launch/hooks/useGetStakingConstants.ts
index d556f6e5..03eff9b2 100644
--- a/apps/launch/hooks/useGetStakingConstants.ts
+++ b/apps/launch/hooks/useGetStakingConstants.ts
@@ -36,6 +36,19 @@ const useStakingContractConstant = ({
});
};
+export const useMaxNumServices = ({ address }: { address: Address }) =>
+ useStakingContractConstant({
+ address,
+ name: 'maxNumServices',
+ });
+
+export const useRewardsPerSecond = ({ address }: { address: Address }) =>
+ useStakingContractConstant({
+ address,
+ name: 'rewardsPerSecond',
+ select: (data) => (typeof data === 'bigint' ? `${Number(formatEther(data))}` : '0'),
+ });
+
export const useGetMinimumStakingDeposit = ({ address }: { address: Address }) =>
useStakingContractConstant({
address,