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,