diff --git a/apps/autonolas-registry/common-util/Contracts/index.jsx b/apps/autonolas-registry/common-util/Contracts/index.jsx index 24eac181..e3fbbd77 100644 --- a/apps/autonolas-registry/common-util/Contracts/index.jsx +++ b/apps/autonolas-registry/common-util/Contracts/index.jsx @@ -1,9 +1,10 @@ import { ethers } from 'ethers'; +import { mainnet } from 'viem/chains'; import Web3 from 'web3'; import { isL1Network } from '@autonolas/frontend-library'; -import { TOKENOMICS } from 'libs/util-contracts/src/lib/abiAndAddresses/tokenomics'; +import { DISPENSER, TOKENOMICS } from 'libs/util-contracts/src/lib/abiAndAddresses'; import { AGENT_REGISTRY_CONTRACT, @@ -218,3 +219,13 @@ export const getTokenomicsEthersContract = (address) => { const contract = new ethers.Contract(address, TOKENOMICS.abi, provider); return contract; }; + +/** + * @returns dispenser contract + */ +export const getDispenserContract = () => { + const abi = DISPENSER.abi; + const address = DISPENSER.addresses[mainnet.id]; + const contract = getContract(abi, address); + return contract; +}; diff --git a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/RewardsSection.tsx b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/RewardsSection.tsx index 41d8221f..497f22ab 100644 --- a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/RewardsSection.tsx +++ b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/RewardsSection.tsx @@ -1,43 +1,89 @@ -import { Col, Flex, Row } from 'antd'; -import { FC, useEffect, useState } from 'react'; +import { Button, Flex, Table, Typography } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Address } from 'viem'; import { getPendingIncentives, useClaimableIncentives } from 'libs/common-contract-functions/src'; import { UNICODE_SYMBOLS } from 'libs/util-constants/src/lib/symbols'; import { TOKENOMICS } from 'libs/util-contracts/src'; +import { notifyError, notifySuccess } from 'libs/util-functions/src'; import { getEthersProviderForEthereum, getTokenomicsEthersContract } from 'common-util/Contracts'; +import { claimOwnerIncentivesRequest } from 'common-util/functions/requests'; +import { useHelpers } from 'common-util/hooks'; -import { RewardsStatistic } from '../styles'; -import { useTokenomicsUnitType } from './hooks'; +import { getTokenomicsUnitType } from './utils'; -type RewardsColumnProps = { title: string; statistic: null | string; loading?: boolean }; - -const RewardColumn = ({ title, statistic, loading }: RewardsColumnProps) => ( - - - -); +const { Text } = Typography; type RewardsSectionProps = { ownerAddress: Address; + isOwner: boolean; id: string; type: string; dataTestId: string; }; -export const RewardsSection: FC = ({ ownerAddress, id, type, dataTestId }) => { +type DataSourceItem = { + id: 'claimable' | 'pending'; + label: string; + donations: string; + topUps: string; +}; +const getColumns = ( + canClaim: boolean, + handleClaim: () => void, + isClaimLoading: boolean, +): ColumnsType => [ + { + title: 'Type', + key: 'type', + dataIndex: 'label', + render: (label, { id }) => ( + + {label} + {id === 'claimable' && canClaim && ( + + )} + + ), + width: '40%', + }, + { + title: 'Donations', + key: 'donations', + dataIndex: 'donations', + render: (donations) => {`${donations} ETH`}, + }, + { + title: 'Top Ups', + key: 'topUps', + dataIndex: 'topUps', + render: (topUps) => {`${topUps} OLAS`}, + }, +]; + +export const RewardsSection: FC = ({ ownerAddress, isOwner, id, type, dataTestId }) => { + const { account } = useHelpers(); + + const [isClaimLoading, setIsClaimLoading] = useState(false); const [isPendingIncentivesLoading, setIsPendingIncentivesLoading] = useState(true); const [pendingIncentives, setPendingIncentives] = useState<{ - reward: string; - topUp: string; + pendingReward: string; + pendingTopUp: string; } | null>(null); - const tokenomicsUnitType = useTokenomicsUnitType(type); + const tokenomicsUnitType = getTokenomicsUnitType(type); + const { - isFetching, reward: claimableReward, - topUp: claimableTopup, + rewardWei: claimableRewardWei, + topUp: claimableTopUp, + topUpWei: claimableTopUpWei, + isFetching: isClaimableIncentivesLoading, + refetch, } = useClaimableIncentives( TOKENOMICS.addresses[1], TOKENOMICS.abi, @@ -46,52 +92,99 @@ export const RewardsSection: FC = ({ ownerAddress, id, type tokenomicsUnitType, ); - useEffect(() => { + const fetchPendingIncentives = useCallback(async () => { const provider = getEthersProviderForEthereum(); const contract = getTokenomicsEthersContract(TOKENOMICS.addresses[1]); - getPendingIncentives(provider, contract, `${tokenomicsUnitType}`, id) - .then((data) => setPendingIncentives(data)) - .catch((error) => console.error(error)) - .finally(() => setIsPendingIncentivesLoading(false)); - }, [ownerAddress, id, tokenomicsUnitType]); + try { + const incentives = await getPendingIncentives({ + provider, + contract, + unitType: `${tokenomicsUnitType}`, + unitId: id, + }); + setPendingIncentives(incentives); + } catch (error) { + console.error(error); + } finally { + setIsPendingIncentivesLoading(false); + } + }, [tokenomicsUnitType, id]); + + useEffect(() => { + fetchPendingIncentives(); + }, [fetchPendingIncentives]); + + const rewards = useMemo(() => { + if (isClaimableIncentivesLoading || isPendingIncentivesLoading) return []; + if (claimableReward == undefined || claimableTopUp === undefined || pendingIncentives === null) + return []; + + return [ + { + id: 'claimable', + label: 'Claimable', + donations: claimableReward, + topUps: claimableTopUp, + }, + { + id: 'pending', + label: 'Pending, next epoch', + donations: pendingIncentives.pendingReward, + topUps: pendingIncentives.pendingTopUp, + }, + ] as DataSourceItem[]; + }, [ + isClaimableIncentivesLoading, + isPendingIncentivesLoading, + claimableReward, + claimableTopUp, + pendingIncentives, + ]); + + const canClaim = useMemo(() => { + if (!isOwner) return false; + if (claimableRewardWei === undefined || claimableTopUpWei === undefined) return false; + + return claimableRewardWei > 0 || claimableTopUpWei > 0; + }, [isOwner, claimableRewardWei, claimableTopUpWei]); + + const handleClaim = useCallback(async () => { + if (!account) return; + if (!canClaim) return; + + try { + setIsClaimLoading(true); + const params = { + account: account as Address, + unitIds: [id], + unitTypes: [`${tokenomicsUnitType}`], + }; + + await claimOwnerIncentivesRequest(params); + notifySuccess('Rewards claimed'); + refetch(); + fetchPendingIncentives(); + } catch (error) { + notifyError(); + console.error(error); + } finally { + setIsClaimLoading(false); + } + }, [account, id, tokenomicsUnitType, canClaim, fetchPendingIncentives, refetch]); return ( - - - - - - - - - - - - - - - - Make donation {UNICODE_SYMBOLS.EXTERNAL_LINK} - - - + + + Make donation {UNICODE_SYMBOLS.EXTERNAL_LINK} + ); }; diff --git a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/index.jsx b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/index.jsx index 976143d2..93165a14 100644 --- a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/index.jsx +++ b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/index.jsx @@ -20,7 +20,7 @@ import { getTokenDetailsRequest } from '../utils'; import { RewardsSection } from './RewardsSection'; import { ServiceStatus } from './ServiceStatus'; import { ViewHashAndCode } from './ViewHashAndCode'; -import { useTokenomicsUnitType } from './hooks'; +import { getTokenomicsUnitType } from './utils'; const navTypesForRewards = [NAV_TYPES.COMPONENT, NAV_TYPES.AGENT]; @@ -56,7 +56,7 @@ export const DetailsSubInfo = ({ const { operatorWhitelistTitle, operatorWhitelistValue, operatorStatusValue } = useOperatorWhitelistComponent(id); - const tokenomicsUnitType = useTokenomicsUnitType(type); + const tokenomicsUnitType = getTokenomicsUnitType(type); const viewHashAndCodeButtons = ( ) : ( {value} @@ -231,7 +232,7 @@ export const DetailsSubInfo = ({ ); }), - [detailsValues, ownerAddress, id, type], + [detailsValues, ownerAddress, isOwner, id, type], ); // get token address for service diff --git a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/hooks.ts b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/utils.ts similarity index 82% rename from apps/autonolas-registry/common-util/Details/DetailsSubInfo/hooks.ts rename to apps/autonolas-registry/common-util/Details/DetailsSubInfo/utils.ts index 3b0801d6..1071a62b 100644 --- a/apps/autonolas-registry/common-util/Details/DetailsSubInfo/hooks.ts +++ b/apps/autonolas-registry/common-util/Details/DetailsSubInfo/utils.ts @@ -2,7 +2,7 @@ import { TOKENOMICS_UNIT_TYPES } from 'libs/util-constants/src'; import { NAV_TYPES } from '../../../util/constants'; -export const useTokenomicsUnitType = (type?: string) => { +export const getTokenomicsUnitType = (type?: string) => { if (type === NAV_TYPES.COMPONENT) return TOKENOMICS_UNIT_TYPES.COMPONENT; if (type === NAV_TYPES.AGENT) return TOKENOMICS_UNIT_TYPES.AGENT; return; diff --git a/apps/autonolas-registry/common-util/Details/index.tsx b/apps/autonolas-registry/common-util/Details/index.tsx index 6df25f94..d1bbbe7d 100644 --- a/apps/autonolas-registry/common-util/Details/index.tsx +++ b/apps/autonolas-registry/common-util/Details/index.tsx @@ -2,6 +2,7 @@ import { Button, Col, Row, Typography } from 'antd'; import capitalize from 'lodash/capitalize'; import get from 'lodash/get'; import { FC, useCallback, useState } from 'react'; +import { Address } from 'viem'; import { GenericObject, Loader, NA } from '@autonolas/frontend-library'; @@ -20,9 +21,9 @@ const { Text } = Typography; type DetailsProps = { id: string; type: NavTypesValues; - getDetails: (id: string) => Promise; // TODO: Define the return type - getTokenUri?: (id: string) => Promise; - getOwner?: (id: string) => Promise; + getDetails: (id: string) => Promise<{ unitHash: Address; dependencies: string[] }>; + getTokenUri: (id: string) => Promise; + getOwner: (id: string) => Promise; handleUpdate?: () => void; handleHashUpdate?: () => void; navigateToDependency?: (id: string, type: NavTypesValues) => void; diff --git a/apps/autonolas-registry/common-util/Details/styles.jsx b/apps/autonolas-registry/common-util/Details/styles.tsx similarity index 75% rename from apps/autonolas-registry/common-util/Details/styles.jsx rename to apps/autonolas-registry/common-util/Details/styles.tsx index 6345cd85..8fd27d73 100644 --- a/apps/autonolas-registry/common-util/Details/styles.jsx +++ b/apps/autonolas-registry/common-util/Details/styles.tsx @@ -1,4 +1,4 @@ -import { Image, Statistic, Typography } from 'antd'; +import { Image, Typography } from 'antd'; import styled from 'styled-components'; import { COLOR } from 'libs/ui-theme/src'; @@ -74,25 +74,4 @@ export const SectionContainer = styled.div` .ant-form-item-label > label { left: -4px; } -`; - -export const RewardsStatistic = styled((props) => )` - .ant-statistic-title { - font-size: 14px; - line-height: 20px; - } - .ant-statistic-content { - font-size: 24px; - font-style: normal; - font-weight: 700; - line-height: 32px; - letter-spacing: -0.72px; - } - .ant-skeleton-title { - margin: 0; - max-width: 200px; - } - .ant-statistic-skeleton { - padding-top: 12px; - } -`; +`; \ No newline at end of file diff --git a/apps/autonolas-registry/common-util/Details/useDetails.jsx b/apps/autonolas-registry/common-util/Details/useDetails.ts similarity index 71% rename from apps/autonolas-registry/common-util/Details/useDetails.jsx rename to apps/autonolas-registry/common-util/Details/useDetails.ts index 7b85c64f..a81d81bc 100644 --- a/apps/autonolas-registry/common-util/Details/useDetails.jsx +++ b/apps/autonolas-registry/common-util/Details/useDetails.ts @@ -1,18 +1,34 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Address } from 'viem'; -import { NA, areAddressesEqual, notifyError } from '@autonolas/frontend-library'; +import { NA } from 'libs/util-constants/src/lib/symbols'; +import { areAddressesEqual, notifyError } from 'libs/util-functions/src'; + +import { NavTypesValues } from 'util/constants'; import { useHelpers } from '../hooks'; import { useSvmConnectivity } from '../hooks/useSvmConnectivity'; -export const useDetails = ({ id, type, getDetails, getOwner, getTokenUri }) => { +export const useDetails = ({ + id, + type, + getDetails, + getOwner, + getTokenUri, +}: { + id: string; + type: NavTypesValues; + getDetails: (id: string) => Promise<{ unitHash: Address; dependencies: string[] }>; + getTokenUri: (id: string) => Promise; + getOwner: (id: string) => Promise; +}) => { const { account, chainId, isSvm } = useHelpers(); const { walletPublicKey } = useSvmConnectivity(); const [isLoading, setIsLoading] = useState(true); const [info, setInfo] = useState({}); const [ownerAddress, setDetailsOwner] = useState(NA); - const [tokenUri, setTokenUri] = useState(null); + const [tokenUri, setTokenUri] = useState(null); // fetch details such as service details, owner of agent/component/service, // token uri @@ -57,13 +73,13 @@ export const useDetails = ({ id, type, getDetails, getOwner, getTokenUri }) => { const isOwner = useMemo(() => { if (isSvm) { if (walletPublicKey && ownerAddress) { - return areAddressesEqual(walletPublicKey, ownerAddress); + return areAddressesEqual(walletPublicKey.toString(), ownerAddress); } return false; } if (account && ownerAddress) { - return areAddressesEqual(account, ownerAddress); + return areAddressesEqual(account.toString(), ownerAddress); } return false; diff --git a/apps/autonolas-registry/common-util/Details/utils.jsx b/apps/autonolas-registry/common-util/Details/utils.ts similarity index 75% rename from apps/autonolas-registry/common-util/Details/utils.jsx rename to apps/autonolas-registry/common-util/Details/utils.ts index 7d2c7ca3..d42823ab 100644 --- a/apps/autonolas-registry/common-util/Details/utils.jsx +++ b/apps/autonolas-registry/common-util/Details/utils.ts @@ -1,14 +1,16 @@ +import { Address } from 'viem'; + import { getOperatorWhitelistContract, getServiceRegistryTokenUtilityContract } from '../Contracts'; import { sendTransaction } from '../functions'; -export const getTokenDetailsRequest = async (serviceId) => { +export const getTokenDetailsRequest = async (serviceId: string) => { const contract = getServiceRegistryTokenUtilityContract(); const deposit = await contract.methods.mapServiceIdTokenDeposit(serviceId).call(); return deposit; }; /* ----- operator whitelist functions ----- */ -export const checkIfServiceRequiresWhitelisting = async (serviceId) => { +export const checkIfServiceRequiresWhitelisting = async (serviceId: string) => { const contract = getOperatorWhitelistContract(); // if true: it is whitelisted by default // else we can whitelist using the input field @@ -16,13 +18,21 @@ export const checkIfServiceRequiresWhitelisting = async (serviceId) => { return response; }; -export const checkIfServiceIsWhitelisted = async (serviceId, operatorAddress) => { +export const checkIfServiceIsWhitelisted = async (serviceId: string, operatorAddress: Address) => { const contract = getOperatorWhitelistContract(); const response = await contract.methods.isOperatorWhitelisted(serviceId, operatorAddress).call(); return response; }; -export const setOperatorsCheckRequest = async ({ account, serviceId, isChecked }) => { +export const setOperatorsCheckRequest = async ({ + account, + serviceId, + isChecked, +}: { + account: Address; + serviceId: string; + isChecked: boolean; +}) => { const contract = getOperatorWhitelistContract(); const fn = contract.methods.setOperatorsCheck(serviceId, isChecked).send({ from: account }); const response = await sendTransaction(fn, account); @@ -34,6 +44,11 @@ export const setOperatorsStatusesRequest = async ({ serviceId, operatorAddresses, operatorStatuses, +}: { + account: Address; + serviceId: string; + operatorAddresses: Address[]; + operatorStatuses: boolean[]; }) => { const contract = getOperatorWhitelistContract(); const fn = contract.methods diff --git a/apps/autonolas-registry/common-util/Login/config.tsx b/apps/autonolas-registry/common-util/Login/config.tsx index 90876702..3990910e 100644 --- a/apps/autonolas-registry/common-util/Login/config.tsx +++ b/apps/autonolas-registry/common-util/Login/config.tsx @@ -83,7 +83,7 @@ export const EVM_SUPPORTED_CHAINS = SUPPORTED_CHAINS.map((chain) => { }; }); -type SolanaChain = { +export type SolanaChain = { id: number | null; networkDisplayName: string; networkName: string; diff --git a/apps/autonolas-registry/common-util/functions/index.jsx b/apps/autonolas-registry/common-util/functions/index.ts similarity index 71% rename from apps/autonolas-registry/common-util/functions/index.jsx rename to apps/autonolas-registry/common-util/functions/index.ts index dd28e049..8a2a1a34 100644 --- a/apps/autonolas-registry/common-util/functions/index.jsx +++ b/apps/autonolas-registry/common-util/functions/index.ts @@ -1,6 +1,9 @@ import { PublicKey } from '@solana/web3.js'; -import { ethers } from 'ethers'; +import { RuleObject } from 'antd/es/form'; +import { StoreValue } from 'antd/es/form/interface'; +import { Contract, FallbackProvider, JsonRpcProvider, ethers } from 'ethers'; import { isString } from 'lodash'; +import { Address } from 'viem'; import { getChainIdOrDefaultToMainnet as getChainIdOrDefaultToMainnetFn, @@ -8,17 +11,23 @@ import { isValidAddress, notifyError, notifyWarning, - sendTransaction as sendTransactionFn, } from '@autonolas/frontend-library'; +import { sendTransaction as sendTransactionFn } from 'libs/util-functions/src'; +import { RpcUrl } from 'libs/util-functions/src/lib/sendTransaction/types'; + import { VM_TYPE } from '../../util/constants'; import { RPC_URLS } from '../Contracts'; import { SUPPORTED_CHAINS } from '../Login'; -import { EVM_SUPPORTED_CHAINS, SVM_SUPPORTED_CHAINS } from '../Login/config'; +import { EVM_SUPPORTED_CHAINS, SVM_SUPPORTED_CHAINS, SolanaChain } from '../Login/config'; -export const getModalProvider = () => window?.MODAL_PROVIDER; +// TODO: provide types for MODAL_PROVIDER +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getModalProvider = () => (window as any)?.MODAL_PROVIDER; -export const getWindowEthereum = () => window?.ethereum; +// TODO: provide types for ethereum +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getWindowEthereum = () => (window as any)?.ethereum; export const getChainId = (chainId = null) => { if (chainId) return chainId; @@ -37,7 +46,7 @@ export const getChainId = (chainId = null) => { export const getProvider = () => { const defaultChainId = getChainId(); - const rpcUrl = RPC_URLS[defaultChainId]; + const rpcUrl = typeof defaultChainId === 'number' ? (RPC_URLS as RpcUrl)[defaultChainId] : null; if (!rpcUrl) { throw new Error(`No RPC URL found for chainId: ${defaultChainId}`); @@ -83,9 +92,10 @@ export const getEthersProvider = () => { return new ethers.FallbackProvider([provider]); }; -export const getIsValidChainId = (chainId) => getIsValidChainIdFn(SUPPORTED_CHAINS, chainId); +export const getIsValidChainId = (chainId: string | number) => + getIsValidChainIdFn(SUPPORTED_CHAINS, chainId); -export const getChainIdOrDefaultToMainnet = (chainId) => { +export const getChainIdOrDefaultToMainnet = (chainId: string | number) => { const x = getChainIdOrDefaultToMainnetFn(SUPPORTED_CHAINS, chainId); return x; }; @@ -98,7 +108,11 @@ export const getChainIdOrDefaultToMainnet = (chainId) => { * @param {object} builderIns - The object to check. * @returns {boolean} - True if the object is a MethodsBuilder object, false otherwise. */ -const isMethodsBuilderInstance = (builderIns, registryAddress) => { +const isMethodsBuilderInstance = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + builderIns: object & { _args?: any }, // TODO: provide better type + registryAddress: Address, +) => { if (typeof builderIns !== 'object' || builderIns === null) { throw new Error('sendTransaction: Input must be an object.'); } @@ -129,9 +143,13 @@ const isMethodsBuilderInstance = (builderIns, registryAddress) => { * @param {string} extra.registryAddress - The address of the registry contract. * */ -export const sendTransaction = (method, account, extra) => { +export const sendTransaction = ( + method: Contract, + account: Address, + extra?: { vmType: string; registryAddress: Address }, +) => { const { vmType, registryAddress } = extra || {}; - if (vmType === VM_TYPE.SVM) { + if (vmType === VM_TYPE.SVM && registryAddress) { // Check if something resembling an SVM method is being passed if (!isMethodsBuilderInstance(method, registryAddress)) { notifyError('Invalid method object'); @@ -142,12 +160,12 @@ export const sendTransaction = (method, account, extra) => { return sendTransactionFn(method, account, { supportedChains: SUPPORTED_CHAINS, - rpcUrls: RPC_URLS, + rpcUrls: RPC_URLS as RpcUrl, }); }; export const addressValidator = () => ({ - validator(_, value) { + validator(_: RuleObject, value: StoreValue) { return isValidAddress(value) ? Promise.resolve() : Promise.reject(new Error('Please enter valid addresses.')); @@ -155,7 +173,10 @@ export const addressValidator = () => ({ }); // check if the provider is gnosis safe -export const checkIfGnosisSafe = async (account, provider) => { +export const checkIfGnosisSafe = async ( + account: Address, + provider: JsonRpcProvider | FallbackProvider, +) => { try { if (provider && provider.getCode) { // TODO: getCode has some issues and throws error in console @@ -166,6 +187,7 @@ export const checkIfGnosisSafe = async (account, provider) => { console.error(error); return false; } + return false; }; /** @@ -174,12 +196,12 @@ export const checkIfGnosisSafe = async (account, provider) => { * but now all networks have service manager token. Hence, this function * defaults to true BUT can be overridden for specific networks in the future. */ -export const doesNetworkHaveValidServiceManagerTokenFn = (chainId) => !!chainId; +export const doesNetworkHaveValidServiceManagerTokenFn = (chainId: number) => !!chainId; -const doesPathIncludesComponents = (path) => !!path?.includes('components'); -const doesPathIncludesAgents = (path) => !!path?.includes('agents'); -export const doesPathIncludesServices = (path) => !!path?.includes('services'); -export const doesPathIncludesComponentsOrAgents = (path) => { +const doesPathIncludesComponents = (path: string) => !!path?.includes('components'); +const doesPathIncludesAgents = (path: string) => !!path?.includes('agents'); +export const doesPathIncludesServices = (path: string) => !!path?.includes('services'); +export const doesPathIncludesComponentsOrAgents = (path: string) => { if (!path) return false; return doesPathIncludesComponents(path) || doesPathIncludesAgents(path); }; @@ -189,15 +211,16 @@ export const notifyWrongNetwork = () => { }; // functions for solana -export const isPageWithSolana = (path) => { +export const isPageWithSolana = (path: string) => { if (!path) return false; if (!isString(path)) return false; - const checkPath = (chain) => path.toLowerCase().includes(chain.networkName.toLowerCase()); + const checkPath = (chain: SolanaChain) => + path.toLowerCase().includes(chain.networkName.toLowerCase()); return SVM_SUPPORTED_CHAINS.some(checkPath); }; -export const isValidSolanaPublicKey = (publicKey) => { +export const isValidSolanaPublicKey = (publicKey: PublicKey) => { try { const isValid = PublicKey.isOnCurve(publicKey); return isValid; diff --git a/apps/autonolas-registry/common-util/functions/requests.jsx b/apps/autonolas-registry/common-util/functions/requests.ts similarity index 53% rename from apps/autonolas-registry/common-util/functions/requests.jsx rename to apps/autonolas-registry/common-util/functions/requests.ts index fa2009ce..9e3c2a6a 100644 --- a/apps/autonolas-registry/common-util/functions/requests.jsx +++ b/apps/autonolas-registry/common-util/functions/requests.ts @@ -1,7 +1,11 @@ -import { notifyError } from '@autonolas/frontend-library'; +import { Address } from 'viem'; + +import { getEstimatedGasLimit, notifyError } from 'libs/util-functions/src'; + +import { sendTransaction } from 'common-util/functions'; import { DEFAULT_SERVICE_CREATION_ETH_TOKEN_ZEROS } from '../../util/constants'; -import { getServiceOwnerMultisigContract } from '../Contracts'; +import { getDispenserContract, getServiceOwnerMultisigContract } from '../Contracts'; import { checkIfGnosisSafe, getEthersProvider } from './index'; const FALLBACK_HANDLER_STORAGE_SLOT = @@ -12,7 +16,7 @@ const FALLBACK_HANDLER_STORAGE_SLOT = * BE code: https://github.com/valory-xyz/autonolas-registries/pull/54#discussion_r1031510182 * @returns {Promise} true if the owner address can mint */ -export const checkIfERC721Receive = async (account, ownerAddress) => { +export const checkIfERC721Receive = async (account: Address, ownerAddress: Address) => { const provider = getEthersProvider(); const isSafe = await checkIfGnosisSafe(account, provider); @@ -23,6 +27,9 @@ export const checkIfERC721Receive = async (account, ownerAddress) => { const owners = await contract.methods.getOwners().call(); if (Number(threshold) > 0 && owners.length > 0) { + // TODO: check and fix error: Property 'getStorageAt' does not exist on type 'JsonRpcProvider | FallbackProvider'. + // Did you mean 'getStorage'? + // @ts-expect-error next-line const contents = await provider.getStorageAt(account, FALLBACK_HANDLER_STORAGE_SLOT); const isInvalidContent = @@ -43,3 +50,26 @@ export const checkIfERC721Receive = async (account, ownerAddress) => { return true; }; + +export const claimOwnerIncentivesRequest = async ({ + account, + unitTypes, + unitIds, +}: { + account: Address; + unitTypes: string[]; + unitIds: string[]; +}) => { + const contract = getDispenserContract(); + try { + const claimFn = contract.methods.claimOwnerIncentives(unitTypes, unitIds); + const estimatedGas = await getEstimatedGasLimit(claimFn, account); + const fn = claimFn.send({ from: account, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account); + return response?.transactionHash; + } catch (error) { + window.console.log('Error occurred on claiming owner incentives'); + throw error; + } +}; diff --git a/apps/autonolas-registry/components/ListServices/index.jsx b/apps/autonolas-registry/components/ListServices/index.jsx index 2322b875..07c09f42 100644 --- a/apps/autonolas-registry/components/ListServices/index.jsx +++ b/apps/autonolas-registry/components/ListServices/index.jsx @@ -1,25 +1,18 @@ -import { useState, useEffect } from 'react'; import { Tabs } from 'antd'; import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + import { notifyError } from '@autonolas/frontend-library'; -import { NAV_TYPES } from '../../util/constants'; -import { - ListTable, - useExtraTabContent, - isMyTab, -} from '../../common-util/List/ListTable'; import { getMyListOnPagination } from '../../common-util/ContractUtils/myList'; +import { ListTable, isMyTab, useExtraTabContent } from '../../common-util/List/ListTable'; import { useHelpers } from '../../common-util/hooks'; -import { - useAllServices, - useMyServices, - useSearchServices, -} from './hooks/useServicesList'; +import { NAV_TYPES } from '../../util/constants'; +import { useAllServices, useMyServices, useSearchServices } from './hooks/useServicesList'; import { useServiceInfo } from './hooks/useSvmService'; import { - getServices, getFilteredServices, + getServices, getTotalForAllServices, getTotalForMyServices, } from './utils'; @@ -29,9 +22,7 @@ const MY_SERVICES = 'my-services'; const ListServices = () => { const router = useRouter(); - const [currentTab, setCurrentTab] = useState( - isMyTab(router) ? MY_SERVICES : ALL_SERVICES, - ); + const [currentTab, setCurrentTab] = useState(isMyTab(router) ? MY_SERVICES : ALL_SERVICES); const { account, links, isSvm, chainId, isMainnet } = useHelpers(); @@ -58,12 +49,8 @@ const ListServices = () => { const [currentPage, setCurrentPage] = useState(1); const [list, setList] = useState([]); - const { - getTotalForAllSvmServices, - getTotalForMySvmServices, - getSvmServices, - getMySvmServices, - } = useServiceInfo(); + const { getTotalForAllSvmServices, getTotalForMySvmServices, getSvmServices, getMySvmServices } = + useServiceInfo(); // fetch total (All services & My services) useEffect(() => { @@ -72,9 +59,7 @@ const ListServices = () => { let totalTemp = null; if (currentTab === ALL_SERVICES) { - totalTemp = isSvm - ? await getTotalForAllSvmServices() - : await getTotalForAllServices(); + totalTemp = isSvm ? await getTotalForAllSvmServices() : await getTotalForAllServices(); } else if (currentTab === MY_SERVICES && account) { totalTemp = isSvm ? await getTotalForMySvmServices(account) @@ -207,15 +192,7 @@ const ListServices = () => { setIsLoading(false); } })(); - }, [ - account, - searchValue, - currentTab, - currentPage, - getServicesBySearch, - isMainnet, - chainId, - ]); + }, [account, searchValue, currentTab, currentPage, getServicesBySearch, isMainnet, chainId]); const tableCommonProps = { type: NAV_TYPES.SERVICE, @@ -225,16 +202,13 @@ const ListServices = () => { setCurrentPage, onViewClick, searchValue, - onUpdateClick: (serviceId) => - router.push(`${links.UPDATE_SERVICE}/${serviceId}`), + onUpdateClick: (serviceId) => router.push(`${links.UPDATE_SERVICE}/${serviceId}`), }; const getMyServiceList = () => { if (isMainnet) return list; - return searchValue - ? list - : getMyListOnPagination({ total, nextPage: currentPage, list }); + return searchValue ? list : getMyListOnPagination({ total, nextPage: currentPage, list }); }; return ( @@ -265,11 +239,7 @@ const ListServices = () => { label: 'All', disabled: isLoading, children: ( - + ), }, { diff --git a/apps/autonolas-registry/tests/components/ListAgents/details.test.jsx b/apps/autonolas-registry/tests/components/ListAgents/details.test.jsx index 70bd07eb..8e91c154 100644 --- a/apps/autonolas-registry/tests/components/ListAgents/details.test.jsx +++ b/apps/autonolas-registry/tests/components/ListAgents/details.test.jsx @@ -37,11 +37,13 @@ jest.mock('wagmi', () => ({ jest.mock('libs/common-contract-functions/src', () => ({ useClaimableIncentives: jest.fn().mockReturnValue({ reward: '0.85', + rewardWei: 850000000000000000, topUp: '222,777.30', + topUpWei: 222777300000000000000000000, }), getPendingIncentives: jest.fn().mockResolvedValue({ - reward: '0.95555', - topUp: '200,500.65', + pendingReward: '0.95555', + pendingTopUp: '200,500.65', }), })); @@ -136,15 +138,15 @@ describe('listAgents/details.jsx', () => { }); it('should display rewards section', async () => { - const { getByText } = render(wrapProvider()); + const { getByText, queryByRole } = render(wrapProvider()); await waitFor(() => { - expect(getByText('Claimable Reward')).toBeInTheDocument(); - expect(getByText('Claimable Top Up')).toBeInTheDocument(); + expect(getByText('Claimable')).toBeInTheDocument(); expect(getByText('0.85 ETH')).toBeInTheDocument(); expect(getByText('222,777.30 OLAS')).toBeInTheDocument(); + const claimButton = queryByRole('button', { name: 'Claim' }); + expect(claimButton).toBeInTheDocument(); - expect(getByText('Pending Reward')).toBeInTheDocument(); - expect(getByText('Pending Top Up')).toBeInTheDocument(); + expect(getByText('Pending, next epoch')).toBeInTheDocument(); expect(getByText('0.95555 ETH')).toBeInTheDocument(); expect(getByText('200,500.65 OLAS')).toBeInTheDocument(); }); diff --git a/apps/autonolas-registry/tests/components/ListComponents/details.test.jsx b/apps/autonolas-registry/tests/components/ListComponents/details.test.jsx index 19c264a7..b076037d 100644 --- a/apps/autonolas-registry/tests/components/ListComponents/details.test.jsx +++ b/apps/autonolas-registry/tests/components/ListComponents/details.test.jsx @@ -38,11 +38,13 @@ jest.mock('wagmi', () => ({ jest.mock('libs/common-contract-functions/src', () => ({ useClaimableIncentives: jest.fn().mockReturnValue({ reward: '0.25', + rewardWei: 250000000000000000, topUp: '111,555.70', + topUpWei: 111555700000000000000000000, }), getPendingIncentives: jest.fn().mockResolvedValue({ - reward: '0.5', - topUp: '100,800.25', + pendingReward: '0.5', + pendingTopUp: '100,800.25', }), })); @@ -128,15 +130,15 @@ describe('listComponents/details.jsx', () => { }); it('should display rewards section', async () => { - const { getByText } = render(wrapProvider()); + const { getByText, queryByRole } = render(wrapProvider()); await waitFor(() => { - expect(getByText('Claimable Reward')).toBeInTheDocument(); - expect(getByText('Claimable Top Up')).toBeInTheDocument(); + expect(getByText('Claimable')).toBeInTheDocument(); expect(getByText('0.25 ETH')).toBeInTheDocument(); expect(getByText('111,555.70 OLAS')).toBeInTheDocument(); + const claimButton = queryByRole('button', { name: 'Claim' }); + expect(claimButton).toBeInTheDocument(); - expect(getByText('Pending Reward')).toBeInTheDocument(); - expect(getByText('Pending Top Up')).toBeInTheDocument(); + expect(getByText('Pending, next epoch')).toBeInTheDocument(); expect(getByText('0.5 ETH')).toBeInTheDocument(); expect(getByText('100,800.25 OLAS')).toBeInTheDocument(); }); diff --git a/apps/govern/common-util/functions/requests.ts b/apps/govern/common-util/functions/requests.ts index f4d3de8b..b673fb4d 100644 --- a/apps/govern/common-util/functions/requests.ts +++ b/apps/govern/common-util/functions/requests.ts @@ -1,20 +1,28 @@ import { readContract, readContracts } from '@wagmi/core'; import { ethers } from 'ethers'; -import { AbiFunction, TransactionReceipt, parseUnits } from 'viem'; +import { Abi, AbiFunction, parseUnits } from 'viem'; import { Address } from 'viem'; import { mainnet } from 'viem/chains'; -import { sendTransaction } from '@autonolas/frontend-library'; - -import { STAKING_FACTORY, VE_OLAS } from 'libs/util-contracts/src/lib/abiAndAddresses'; -import { getEstimatedGasLimit } from 'libs/util-functions/src'; +import { + SERVICE_REGISTRY, + STAKING_FACTORY, + VE_OLAS, +} from 'libs/util-contracts/src/lib/abiAndAddresses'; +import { getEstimatedGasLimit, sendTransaction } from 'libs/util-functions/src'; import { SUPPORTED_CHAINS, wagmiConfig } from 'common-util/config/wagmi'; import { RPC_URLS } from 'common-util/constants/rpcs'; import { getAddressFromBytes32 } from './addresses'; import { getUnixNextWeekStartTimestamp } from './time'; -import { getOlasContract, getVeOlasContract, getVoteWeightingContract } from './web3'; +import { + getOlasContract, + getTokenomicsContract, + getTreasuryContract, + getVeOlasContract, + getVoteWeightingContract, +} from './web3'; type VoteForNomineeWeightsParams = { account: Address | undefined; @@ -198,7 +206,7 @@ export const createLockRequest = async ({ rpcUrls: RPC_URLS, }); - return (response as TransactionReceipt)?.transactionHash; + return response?.transactionHash; } catch (error) { window.console.log('Error occurred on creating lock for veOLAS'); throw error; @@ -227,7 +235,7 @@ export const updateIncreaseAmount = async ({ rpcUrls: RPC_URLS, }); - return (response as TransactionReceipt)?.transactionHash; + return response?.transactionHash; } catch (e) { window.console.log('Error occurred on increasing amount with estimated gas'); throw e; @@ -259,7 +267,7 @@ export const updateIncreaseUnlockTime = async ({ rpcUrls: RPC_URLS, }); - return (response as TransactionReceipt)?.transactionHash; + return response?.transactionHash; } catch (error) { window.console.log('Error occurred on increasing unlock time'); throw error; @@ -282,9 +290,95 @@ export const withdrawVeolasRequest = async ({ account }: { account: Address }) = rpcUrls: RPC_URLS, }); - return (response as TransactionReceipt)?.transactionHash; + return response?.transactionHash; } catch (error) { window.console.log('Error occurred on withdrawing veOlas'); throw error; } }; + +/** + * Start new epoch + */ +export const checkpointRequest = async ({ account }: { account: Address }) => { + const contract = getTokenomicsContract(); + try { + const checkpointFn = contract.methods.checkpoint(); + const estimatedGas = await getEstimatedGasLimit(checkpointFn, account); + const fn = checkpointFn.send({ from: account, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return response?.transactionHash; + } catch (error) { + window.console.log('Error occurred on starting new epoch'); + throw error; + } +}; + +/** + * Check services are eligible for donating + */ +export const checkServicesTerminatedOrNotDeployed = async (ids: string[]) => { + const invalidServiceIds: string[] = []; + + try { + const response = await readContracts(wagmiConfig, { + contracts: ids.map((id) => ({ + abi: SERVICE_REGISTRY.abi as Abi, + address: (SERVICE_REGISTRY.addresses as Record)[mainnet.id], + chainId: mainnet.id, + functionName: 'getService', + args: [id], + })), + }); + + response.forEach((service, index) => { + const serviceData = service.result as { state: number } | null; + if (serviceData && serviceData.state !== 4 && serviceData.state !== 5) { + invalidServiceIds.push(ids[index]); + } + }); + } catch (error) { + window.console.log('Error on checking service status'); + throw error; + } + + return invalidServiceIds; +}; + +/** + * Donate to services + */ +export const depositServiceDonationRequest = async ({ + account, + serviceIds, + amounts, + totalAmount, +}: { + account: Address; + serviceIds: string[]; + amounts: string[]; + totalAmount: string; +}) => { + const contract = getTreasuryContract(); + + try { + const depositFn = contract.methods.depositServiceDonationsETH(serviceIds, amounts); + const estimatedGas = await getEstimatedGasLimit(depositFn, account, totalAmount); + const fn = depositFn.send({ from: account, value: totalAmount, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return response?.transactionHash; + } catch (error) { + window.console.log('Error occurred on depositing service donation'); + throw error; + } +}; diff --git a/apps/govern/common-util/functions/web3.ts b/apps/govern/common-util/functions/web3.ts index 3362c14a..0640eb5b 100644 --- a/apps/govern/common-util/functions/web3.ts +++ b/apps/govern/common-util/functions/web3.ts @@ -2,7 +2,13 @@ import { mainnet } from 'viem/chains'; import Web3 from 'web3'; import { AbiItem } from 'web3-utils'; -import { OLAS, VE_OLAS, VOTE_WEIGHTING } from 'libs/util-contracts/src/lib/abiAndAddresses'; +import { + OLAS, + TOKENOMICS, + TREASURY, + VE_OLAS, + VOTE_WEIGHTING, +} from 'libs/util-contracts/src/lib/abiAndAddresses'; import { getChainId, getProvider } from 'common-util/functions/frontend-library'; @@ -46,3 +52,17 @@ export const getVeOlasContract = () => { const contract = getContract(abi, address); return contract; }; + +export const getTokenomicsContract = () => { + const abi = TOKENOMICS.abi as unknown as AbiItem[]; + const address = TOKENOMICS.addresses[mainnet.id]; + const contract = getContract(abi, address); + return contract; +}; + +export const getTreasuryContract = () => { + const abi = TREASURY.abi as AbiItem[]; + const address = TREASURY.addresses[mainnet.id]; + const contract = getContract(abi, address); + return contract; +}; diff --git a/apps/govern/components/Donate/DonateForm.tsx b/apps/govern/components/Donate/DonateForm.tsx new file mode 100644 index 00000000..6c1c535f --- /dev/null +++ b/apps/govern/components/Donate/DonateForm.tsx @@ -0,0 +1,132 @@ +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Grid, InputNumber, Space, Typography } from 'antd'; +import styled from 'styled-components'; +import { useAccount } from 'wagmi'; + +const { Text } = Typography; +const { useBreakpoint } = Grid; + +export const DynamicFormContainer = styled.div` + max-width: 720px; + .ant-input-number { + width: 200px; + } +`; + +type DonateFormProps = { + isLoading: boolean; + onSubmit: ({ unitIds, amounts }: { unitIds: number[]; amounts: number[] }) => Promise; +}; + +export const DonateForm = ({ isLoading, onSubmit }: DonateFormProps) => { + const { address: account } = useAccount(); + const [form] = Form.useForm(); + + const screens = useBreakpoint(); + const inputStyle = screens.xs ? { width: '140px' } : { width: 'auto' }; + + const onFinish = async (values: { units: { unitId: number; amount: number }[] }) => { + try { + await onSubmit({ + unitIds: values.units.map((unit) => unit.unitId), + amounts: values.units.map((unit) => unit.amount), + }); + + form.resetFields(); + } catch (error) { + window.console.error(error); + } + }; + + return ( + +
+ { + if (!units || units?.length === 0) { + return Promise.reject(new Error('At least 1 unit is required')); + } + return Promise.resolve(); + }, + }, + ]} + > + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field) => ( + + prevValues.units !== curValues.units} + > + {() => ( + + + + )} + + + + + + + {fields.length > 1 && remove(field.name)} />} + + ))} + + + + + + + + )} + + + + + + {!account && ( + + To donate, connect a wallet + + )} + + +
+ ); +}; diff --git a/apps/govern/components/Donate/hooks.ts b/apps/govern/components/Donate/hooks.ts new file mode 100644 index 00000000..919621fd --- /dev/null +++ b/apps/govern/components/Donate/hooks.ts @@ -0,0 +1,121 @@ +import { ethers } from 'ethers'; +import { useCallback, useMemo } from 'react'; +import { mainnet } from 'viem/chains'; +import { useReadContract } from 'wagmi'; + +import { TOKENOMICS, TREASURY } from 'libs/util-contracts/src/lib/abiAndAddresses'; + +const useVeOLASThreshold = () => + useReadContract({ + address: TOKENOMICS.addresses[mainnet.id], + abi: TOKENOMICS.abi, + chainId: mainnet.id, + functionName: 'veOLASThreshold', + query: { + select: (data) => ethers.formatEther(`${data}`), + }, + }); + +const useMinAcceptedETH = () => + useReadContract({ + address: TREASURY.addresses[mainnet.id], + abi: TREASURY.abi, + chainId: mainnet.id, + functionName: 'minAcceptedETH', + }); + +const useEpochCounter = () => + useReadContract({ + address: TOKENOMICS.addresses[mainnet.id], + abi: TOKENOMICS.abi, + chainId: mainnet.id, + functionName: 'epochCounter', + }); + +const useEpochTokenomics = (epochCounter: number | undefined) => + useReadContract({ + address: TOKENOMICS.addresses[mainnet.id], + abi: TOKENOMICS.abi, + chainId: mainnet.id, + functionName: 'mapEpochTokenomics', + args: [BigInt(epochCounter || 0)], + query: { + enabled: epochCounter !== undefined, + }, + }); + +const useEpochLength = () => + useReadContract({ + address: TOKENOMICS.addresses[mainnet.id], + abi: TOKENOMICS.abi, + chainId: mainnet.id, + functionName: 'epochLen', + }); + +export const useThresholdData = () => { + const { + data: veOLASThreshold, + isFetching: isVeOLASThresholdFetching, + refetch: refetchVeOLASThreshold, + } = useVeOLASThreshold(); + const { + data: minAcceptedETH, + isFetching: isMinAcceptedETHFetching, + refetch: refetchMinAcceptedETH, + } = useMinAcceptedETH(); + const { + data: epochCounter, + isFetching: isEpochCounterFetching, + refetch: refetchEpochCounter, + } = useEpochCounter(); + const { + data: prevEpochPoint, + isFetching: isPrevEpochPointFetching, + refetch: refetchPrevEpochPoint, + } = useEpochTokenomics(epochCounter !== undefined ? Number(epochCounter) - 1 : undefined); + const { + data: epochLength, + isFetching: isEpochLengthFetching, + refetch: refetchEpochLength, + } = useEpochLength(); + + const nextEpochEndTime = useMemo(() => { + if (prevEpochPoint === undefined) return null; + if (epochLength === undefined) return null; + return prevEpochPoint.endTime + epochLength; + }, [prevEpochPoint, epochLength]); + + const refetchData = useCallback(async () => { + const promises = [ + refetchVeOLASThreshold(), + refetchMinAcceptedETH(), + refetchEpochCounter(), + refetchPrevEpochPoint(), + refetchEpochLength(), + ]; + + return Promise.all(promises); + }, [ + refetchVeOLASThreshold, + refetchMinAcceptedETH, + refetchEpochCounter, + refetchPrevEpochPoint, + refetchEpochLength, + ]); + + return { + veOLASThreshold, + minAcceptedETH: minAcceptedETH as bigint | undefined, + epochCounter, + prevEpochEndTime: prevEpochPoint?.endTime, + epochLength, + nextEpochEndTime, + isDataLoading: + isVeOLASThresholdFetching || + isMinAcceptedETHFetching || + isEpochCounterFetching || + isPrevEpochPointFetching || + isEpochLengthFetching, + refetchData, + }; +}; diff --git a/apps/govern/components/Donate/index.tsx b/apps/govern/components/Donate/index.tsx new file mode 100644 index 00000000..4269a497 --- /dev/null +++ b/apps/govern/components/Donate/index.tsx @@ -0,0 +1,203 @@ +import { Alert, Button, Card, Skeleton, Typography } from 'antd'; +import { ethers } from 'ethers'; +import isNumber from 'lodash/isNumber'; +import Link from 'next/link'; +import { useState } from 'react'; +import { useAccount } from 'wagmi'; + +import { NA, getFullFormattedDate, notifySuccess } from '@autonolas/frontend-library'; + +import { notifyError } from 'libs/util-functions/src'; + +import { + checkServicesTerminatedOrNotDeployed, + checkpointRequest, + depositServiceDonationRequest, +} from 'common-util/functions'; + +import { DonateForm } from './DonateForm'; +import { useThresholdData } from './hooks'; +import { DonateContainer, EpochCheckpointRow, EpochStatus } from './styles'; + +const { Title, Paragraph, Text } = Typography; + +const sortUnitIdsAndAmounts = (unitIds: number[], amounts: number[]) => { + const sortedUnitIds = [...unitIds].sort((a, b) => a - b); + const sortedAmounts = sortedUnitIds.map((e) => amounts[unitIds.indexOf(e)]); + return [sortedUnitIds, sortedAmounts]; +}; + +export const DonatePage = () => { + const { address: account } = useAccount(); + + const [isDonationLoading, setIsDonationLoading] = useState(false); + const [isCheckpointLoading, setIsCheckpointLoading] = useState(false); + + const { + isDataLoading, + refetchData, + veOLASThreshold, + minAcceptedETH, + epochCounter, + prevEpochEndTime, + epochLength, + nextEpochEndTime, + } = useThresholdData(); + + const onDepositServiceDonationSubmit = async (values: { + unitIds: number[]; + amounts: number[]; + }) => { + if (!account) return; + + try { + setIsDonationLoading(true); + + const [sortedUnitIds, sortedAmounts] = sortUnitIdsAndAmounts(values.unitIds, values.amounts); + const serviceIds = sortedUnitIds.map((e) => `${e}`); + const invalidServices = await checkServicesTerminatedOrNotDeployed(serviceIds); + + // deposit only if all services are deployed or not terminated + if (invalidServices.length > 0) { + throw new Error( + `Provided service IDs are not deployed or terminated ${invalidServices.join(', ')}`, + ); + } else { + const amounts = sortedAmounts.map((e) => ethers.parseUnits(`${e}`, 18).toString()); + const totalAmount = amounts.reduce( + (a, b) => ethers.toBigInt(a) + ethers.toBigInt(b), + BigInt(0), + ); + + if (minAcceptedETH !== undefined && minAcceptedETH > totalAmount) { + throw new Error( + `At least ${ethers.formatEther( + `${minAcceptedETH}`, + )} ETH of donations is required to trigger boosts.`, + ); + } else { + const params = { + account, + serviceIds, + amounts, + totalAmount: totalAmount.toString(), + }; + + await depositServiceDonationRequest(params); + notifySuccess('Deposited service donation successfully'); + } + } + } catch (error) { + console.error(error); + const errorMessage = + (error as Error).message || 'Error occurred on depositing service donation'; + notifyError(errorMessage); + throw error; + } finally { + setIsDonationLoading(false); + } + }; + + const onCheckpoint = async () => { + if (!account) return; + + try { + setIsCheckpointLoading(true); + await checkpointRequest({ account }); + await refetchData(); // update epoch details after checkpoint + notifySuccess('Started new epoch'); + } catch (error) { + console.error(error); + notifyError('Error occurred on starting new epoch'); + } finally { + setIsCheckpointLoading(false); + } + }; + + const epochStatusList = [ + { + text: 'Earliest possible expected end time', + value: nextEpochEndTime ? getFullFormattedDate(nextEpochEndTime * 1000) : NA, + }, + { + text: 'Epoch length', + value: isNumber(epochLength) ? `${epochLength / 3600 / 24} days` : NA, + }, + { + text: 'Previous epoch end time', + value: prevEpochEndTime ? getFullFormattedDate(prevEpochEndTime * 1000) : NA, + }, + { + text: 'Epoch counter', + value: epochCounter || NA, + }, + ]; + + // disable checkpoint button if expected end time is in the future + const isExpectedEndTimeInFuture = (nextEpochEndTime || 0) * 1000 > Date.now(); + + return ( + + + + Donate + + + Show appreciation for the value of an autonomous service by making a donation. The + protocol will reward devs who have contributed code for that service. + + + + To boost rewards of devs with freshly minted OLAS, you must hold at least  + {veOLASThreshold || NA} +  veOLAS. Grab your veOLAS by locking OLAS  + here. At least  + + {minAcceptedETH ? ethers.formatEther(`${minAcceptedETH}`) : NA} +  ETH + +  of donations is required to trigger boosts. + + } + className="mb-16" + /> + + + + + + + Epoch Status + + + {epochStatusList.map(({ text, value }, index) => ( + + {`${text}:`} + {isDataLoading ? ( + + ) : ( + {value} + )} + + ))} + + + + New epochs must be manually triggered by community members + + + + ); +}; diff --git a/apps/govern/components/Donate/styles.tsx b/apps/govern/components/Donate/styles.tsx new file mode 100644 index 00000000..484d0ba3 --- /dev/null +++ b/apps/govern/components/Donate/styles.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +import { MEDIA_QUERY } from 'libs/ui-theme/src'; + +export const DonateContainer = styled.div` + display: flex; + gap: 16px; + + .donate-section { + width: 720px; + } + .last-epoch-section { + flex: auto; + } + + ${MEDIA_QUERY.tabletL} { + flex-direction: column; + .donate-section { + width: 100%; + } + .last-epoch-section { + padding-left: 0; + margin-left: 0; + border-left: none; + } + } +`; + +export const EpochStatus = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; + h5, + div { + margin: 0; + } +`; + +export const EpochCheckpointRow = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 1rem; + .ant-btn { + width: 200px; + } +`; diff --git a/apps/govern/components/Layout/Menu.tsx b/apps/govern/components/Layout/Menu.tsx index 6f4cb926..d981bd4a 100644 --- a/apps/govern/components/Layout/Menu.tsx +++ b/apps/govern/components/Layout/Menu.tsx @@ -12,6 +12,7 @@ const items: MenuItem[] = [ { label: 'Staking contracts', key: 'contracts', path: '/contracts' }, { label: 'Proposals', key: 'proposals', path: '/proposals' }, { label: 'veOLAS', key: 'veolas', path: '/veolas' }, + { label: 'Donate', key: 'donate', path: '/donate' }, { label: 'Docs', key: 'docs', path: '/docs' }, ]; diff --git a/apps/govern/components/Proposals/Proposals.spec.tsx b/apps/govern/components/Proposals/index.spec.tsx similarity index 95% rename from apps/govern/components/Proposals/Proposals.spec.tsx rename to apps/govern/components/Proposals/index.spec.tsx index 63d42e29..591ab2e9 100644 --- a/apps/govern/components/Proposals/Proposals.spec.tsx +++ b/apps/govern/components/Proposals/index.spec.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import { UNICODE_SYMBOLS } from 'libs/util-constants/src'; -import { ProposalsPage, proposals } from './Proposals'; +import { ProposalsPage, proposals } from './index'; describe('', () => { it('should display the page title and description', () => { diff --git a/apps/govern/components/Proposals/Proposals.tsx b/apps/govern/components/Proposals/index.tsx similarity index 100% rename from apps/govern/components/Proposals/Proposals.tsx rename to apps/govern/components/Proposals/index.tsx diff --git a/apps/govern/pages/donate.tsx b/apps/govern/pages/donate.tsx new file mode 100644 index 00000000..a655e295 --- /dev/null +++ b/apps/govern/pages/donate.tsx @@ -0,0 +1,3 @@ +import { DonatePage } from 'components/Donate'; + +export default DonatePage; diff --git a/apps/govern/pages/proposals.tsx b/apps/govern/pages/proposals.tsx index 1d63f204..c983755b 100644 --- a/apps/govern/pages/proposals.tsx +++ b/apps/govern/pages/proposals.tsx @@ -1,3 +1,3 @@ -import { ProposalsPage } from 'components/Proposals/Proposals'; +import { ProposalsPage } from 'components/Proposals'; export default ProposalsPage; diff --git a/apps/launch/common-util/functions/web3.ts b/apps/launch/common-util/functions/web3.ts index a8fc49c8..b44fee87 100644 --- a/apps/launch/common-util/functions/web3.ts +++ b/apps/launch/common-util/functions/web3.ts @@ -1,13 +1,14 @@ -import { ContractTransactionReceipt, EventLog } from 'ethers'; +import { BaseContract, EventLog } from 'ethers'; import { Address } from 'viem'; import Web3 from 'web3'; import { AbiItem } from 'web3-utils'; -import { sendTransaction as sendTransactionFn } from '@autonolas/frontend-library'; - import { RPC_URLS } from 'libs/util-constants/src'; import { STAKING_FACTORY, VOTE_WEIGHTING } from 'libs/util-contracts/src/lib/abiAndAddresses'; -import { getEstimatedGasLimit } from 'libs/util-functions/src'; +import { + getEstimatedGasLimit, + sendTransaction as sendTransactionFn, +} from 'libs/util-functions/src'; import { SUPPORTED_CHAINS } from 'common-util/config/wagmi'; import { getChainId, getProvider } from 'common-util/functions/frontend-library'; @@ -82,7 +83,7 @@ export const createStakingContract = async ({ const createFn = contract.methods.createStakingInstance(implementation, initPayload); const result = await sendTransaction(createFn, account); - return result as ContractTransactionReceipt & { + return result as BaseContract & { events?: { InstanceCreated: InstanceCreatedEvent }; }; }; diff --git a/apps/tokenomics/components/Layout/index.jsx b/apps/tokenomics/components/Layout/index.jsx index b8bf8ead..e19f7143 100644 --- a/apps/tokenomics/components/Layout/index.jsx +++ b/apps/tokenomics/components/Layout/index.jsx @@ -45,12 +45,14 @@ const Layout = ({ children }) => { useEffect(() => { if (router.pathname) { const name = router.pathname.split('/')[1]; - setSelectedMenu(name || 'veolas'); + setSelectedMenu(name || 'dev-incentives'); } }, [router.pathname]); const handleMenuItemClick = ({ key }) => { - if (key === 'docs') { + if (key === 'donate') { + window.open('https://govern.olas.network/donate', '_blank'); + } else if (key === 'docs') { window.open('https://docs.autonolas.network/protocol/tokenomics/', '_blank'); } else if (key === 'bonding-products') { window.open('https://bond.olas.network/bonding-products', '_blank'); @@ -80,8 +82,8 @@ const Layout = ({ children }) => { selectedKeys={[selectedMenu]} onClick={handleMenuItemClick} items={[ - { key: 'donate', label: 'Donate' }, { key: 'dev-incentives', label: 'Dev Rewards' }, + { key: 'donate', label: }, { key: 'bonding-products', label: }, { key: 'my-bonds', label: }, { diff --git a/apps/tokenomics/next.config.js b/apps/tokenomics/next.config.js index d05da172..7603316d 100644 --- a/apps/tokenomics/next.config.js +++ b/apps/tokenomics/next.config.js @@ -30,8 +30,13 @@ const nextConfig = { return [ { source: '/', - destination: '/donate', + destination: '/dev-incentives', permanent: false, + }, + { + source: '/donate', + destination: 'https://govern.olas.network/donate', + permanent: true, }, { source: '/bonding-products', diff --git a/libs/common-contract-functions/src/lib/rewards.ts b/libs/common-contract-functions/src/lib/rewards.ts index 2323e2fc..ea691f2e 100644 --- a/libs/common-contract-functions/src/lib/rewards.ts +++ b/libs/common-contract-functions/src/lib/rewards.ts @@ -1,7 +1,5 @@ import { Contract, Provider, ethers } from 'ethers'; -import { TOKENOMICS_UNIT_TYPES } from 'libs/util-constants/src'; - import { rewardsFormatter } from './utils'; const BIG_INT_0 = BigInt(0); @@ -23,10 +21,8 @@ const getEpochDetails = async (provider: Provider, contract: Contract) => { // epoch length const epochLen: bigint = await contract.epochLen(); - // get the block number and timestamp - const blockNumber = await provider.getBlockNumber(); - - const blockTimestamp = (await provider.getBlock(blockNumber))?.timestamp || 0; + // get the block timestamp + const blockTimestamp = (await provider.getBlock('latest'))?.timestamp || 0; const timeDiff = ethers.toBigInt(blockTimestamp) - epTokenomics.endTime; return { timeDiff, epochLen }; @@ -39,7 +35,7 @@ type MapUnitIncentivesRequestArgs = { unitId: string; }; -const getMapUnitIncentivesRequest = async ({ +export const getPendingIncentives = async ({ provider, contract, unitType, @@ -47,71 +43,44 @@ const getMapUnitIncentivesRequest = async ({ }: MapUnitIncentivesRequestArgs) => { const currentEpochCounter = await getEpochCounter(contract); - // Get the unit points of the last epoch - const componentInfo: { - rewardUnitFraction: bigint; - topUpUnitFraction: bigint; - sumUnitTopUpsOLAS: bigint; - } = await contract.getUnitPoint(currentEpochCounter, 0); - const agentInfo: { - rewardUnitFraction: bigint; - topUpUnitFraction: bigint; - sumUnitTopUpsOLAS: bigint; - } = await contract.getUnitPoint(currentEpochCounter, 1); - const { pendingRelativeReward, pendingRelativeTopUp, lastEpoch, }: { - pendingRelativeReward: bigint; - pendingRelativeTopUp: bigint; + pendingRelativeReward: bigint; // Pending relative reward in ETH + pendingRelativeTopUp: bigint; // Pending relative top-up lastEpoch: bigint; } = await contract.mapUnitIncentives(unitType, unitId); - // Struct for component / agent incentive balances - // struct IncentiveBalances { - // uint96 reward; // Reward in ETH [0] - // uint96 pendingRelativeReward; // Pending relative reward in ETH [1] - // uint96 topUp; // Top-up in OLAS [2] - // uint96 pendingRelativeTopUp; // Pending relative top-up [3] - // uint32 lastEpoch; // Last epoch number the information was updated [4] - // } const isCurrentEpochWithReward = currentEpochCounter === lastEpoch && pendingRelativeReward > BIG_INT_0; - // if the current epoch is not the last epoch, return 0 + // if already received rewards this epoch, return zeroes if (!isCurrentEpochWithReward) { return { - pendingRelativeReward: '0.0000', - pendingRelativeTopUp: '0.00', + pendingReward: rewardsFormatter(BigInt(0), 4), + pendingTopUp: rewardsFormatter(BigInt(0), 2), }; } - // if the current epoch is the last epoch, calculate the incentives - const { - rewardUnitFraction: cRewardFraction, - topUpUnitFraction: cTopupFraction, - sumUnitTopUpsOLAS: cSumUnitTopUpsOLAS, - } = componentInfo; - const { - rewardUnitFraction: aRewardFraction, - topUpUnitFraction: aTopupFraction, - sumUnitTopUpsOLAS: aSumUnitTopUpsOLAS, - } = agentInfo; + // Get the unit points of the current epoch + const unitInfo: { + rewardUnitFraction: bigint; + topUpUnitFraction: bigint; + sumUnitTopUpsOLAS: bigint; + } = await contract.getUnitPoint(currentEpochCounter, unitType); - /** - * for unitType agent(0) & component(1), - * the below calculation is done to get the reward and topup - */ - const componentReward = (pendingRelativeReward * cRewardFraction) / BIG_INT_100; - const agentReward = (pendingRelativeReward * aRewardFraction) / BIG_INT_100; + const pendingReward = (pendingRelativeReward * unitInfo.rewardUnitFraction) / BIG_INT_100; let totalIncentives = pendingRelativeTopUp; - let componentTopUp = BigInt(0); - let agentPendingTopUp = BigInt(0); + let pendingTopUp = BigInt(0); - if (pendingRelativeTopUp > BIG_INT_0) { + /** + * the below calculation is done to get the reward and topup + * based on current epoch length + */ + if (totalIncentives > BIG_INT_0) { const inflationPerSecond: bigint = await contract.inflationPerSecond(); const { timeDiff, epochLen } = await getEpochDetails(provider, contract); @@ -120,37 +89,12 @@ const getMapUnitIncentivesRequest = async ({ const totalTopUps = inflationPerSecond * epochLength; totalIncentives = totalIncentives * totalTopUps; - componentTopUp = (totalIncentives * cTopupFraction) / (cSumUnitTopUpsOLAS * BIG_INT_100); - agentPendingTopUp = (totalIncentives * aTopupFraction) / (aSumUnitTopUpsOLAS * BIG_INT_100); + pendingTopUp = + (totalIncentives * unitInfo.topUpUnitFraction) / (unitInfo.sumUnitTopUpsOLAS * BIG_INT_100); } - const isComponent = unitType === TOKENOMICS_UNIT_TYPES.COMPONENT; - const pendingRelativeTopUpInWei = isComponent ? componentReward : agentReward; - const componentTopUpInWei = isComponent ? componentTopUp : agentPendingTopUp; - return { - pendingRelativeReward: rewardsFormatter(pendingRelativeTopUpInWei, 4), - pendingRelativeTopUp: rewardsFormatter(componentTopUpInWei, 2), + pendingReward: rewardsFormatter(pendingReward, 4), + pendingTopUp: rewardsFormatter(pendingTopUp, 2), }; -}; - -// TODO: unable to import TOKENOMICS from util-contracts as of now -// once the import is fixed, remove provider and contract from the function signature -export const getPendingIncentives = async ( - provider: Provider, - contract: Contract, - unitType: string, - unitId: string, -): Promise<{ reward: string; topUp: string }> => { - const { pendingRelativeReward, pendingRelativeTopUp } = await getMapUnitIncentivesRequest({ - provider, - contract, - unitType, - unitId, - }); - - return { - reward: pendingRelativeReward, - topUp: pendingRelativeTopUp, - }; -}; +}; \ No newline at end of file diff --git a/libs/common-contract-functions/src/lib/useRewards.ts b/libs/common-contract-functions/src/lib/useRewards.ts index 211cb26f..76f35699 100644 --- a/libs/common-contract-functions/src/lib/useRewards.ts +++ b/libs/common-contract-functions/src/lib/useRewards.ts @@ -29,7 +29,12 @@ export const useClaimableIncentives = ( enabled: !!ownerAddress && !!id && isNumber(tokenomicsUnitType), select: (data) => { const [reward, topup] = data as [bigint, bigint]; - return { reward: rewardsFormatter(reward, 4), topUp: rewardsFormatter(topup, 2) }; + return { + reward: rewardsFormatter(reward, 4), + rewardWei: reward, + topUp: rewardsFormatter(topup, 2), + topUpWei: topup, + }; }, refetchOnWindowFocus: false, }, diff --git a/libs/util-contracts/src/lib/abiAndAddresses/treasury.js b/libs/util-contracts/src/lib/abiAndAddresses/treasury.ts similarity index 99% rename from libs/util-contracts/src/lib/abiAndAddresses/treasury.js rename to libs/util-contracts/src/lib/abiAndAddresses/treasury.ts index aeb306c3..f8766fb7 100644 --- a/libs/util-contracts/src/lib/abiAndAddresses/treasury.js +++ b/libs/util-contracts/src/lib/abiAndAddresses/treasury.ts @@ -1,4 +1,6 @@ -export const TREASURY = { +import { Contract } from "./types"; + +export const TREASURY: Contract = { contractName: 'Treasury', addresses: { 1: '0xa0DA53447C0f6C4987964d8463da7e6628B30f82', diff --git a/libs/util-functions/src/lib/ethers.ts b/libs/util-functions/src/lib/ethers.ts index f8c97b8d..ab942e5a 100644 --- a/libs/util-functions/src/lib/ethers.ts +++ b/libs/util-functions/src/lib/ethers.ts @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import toLower from 'lodash/toLower'; import { Address } from 'viem'; export const getAddressFromBytes32 = (address: Address | string) => { @@ -10,3 +11,6 @@ export const getBytes32FromAddress = (address: Address | string) => { }; export const isValidAddress = (address: string) => ethers.isAddress(address); + +export const areAddressesEqual = (a1: string | Address, a2: string | Address) => + toLower(a1) === toLower(a2); diff --git a/libs/util-functions/src/lib/requests.ts b/libs/util-functions/src/lib/requests.ts index 36533382..3ac16366 100644 --- a/libs/util-functions/src/lib/requests.ts +++ b/libs/util-functions/src/lib/requests.ts @@ -1,5 +1,6 @@ import { Contract } from 'ethers'; + const ESTIMATED_GAS_LIMIT = 500_000; /** @@ -8,17 +9,18 @@ const ESTIMATED_GAS_LIMIT = 500_000; export const getEstimatedGasLimit = async ( fn: Contract['methods'], account: `0x${string}` | string | undefined, + value?: string, ) => { if (!account) { throw new Error('Invalid account passed to estimate gas limit'); } try { - const estimatedGas = await fn.estimateGas({ from: account }); + const estimatedGas = await fn.estimateGas({ from: account, value }); return Math.ceil(Number(estimatedGas) * 1.2); } catch (error) { window.console.warn(`Error occurred on estimating gas, defaulting to ${ESTIMATED_GAS_LIMIT}`); } return ESTIMATED_GAS_LIMIT; -}; +}; \ No newline at end of file diff --git a/libs/util-functions/src/lib/sendTransaction/helpers.ts b/libs/util-functions/src/lib/sendTransaction/helpers.ts index d11145c2..d4c60df6 100644 --- a/libs/util-functions/src/lib/sendTransaction/helpers.ts +++ b/libs/util-functions/src/lib/sendTransaction/helpers.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ethers } from 'ethers'; + import { getUrl } from './index'; import { Chain, RpcUrl } from './types'; @@ -37,10 +38,7 @@ export const pollTransactionDetails = async (hash: string, chainId: number) => { * i/p: getIsValidChainId(1); * o/p: true */ -export const getIsValidChainId = ( - SUPPORTED_CHAINS: Chain[], - chainId: number | string -) => { +export const getIsValidChainId = (SUPPORTED_CHAINS: Chain[], chainId: number | string) => { if (!chainId) return false; return SUPPORTED_CHAINS.some((e) => e.id === Number(chainId)); @@ -69,9 +67,7 @@ export const getProvider = (supportedChains: Chain[], rpcUrls: RpcUrl) => { } if (typeof window === 'undefined') { - console.warn( - 'No provider found, fetching RPC URL from first supported chain' - ); + console.warn('No provider found, fetching RPC URL from first supported chain'); return rpcUrl; } @@ -82,9 +78,7 @@ export const getProvider = (supportedChains: Chain[], rpcUrls: RpcUrl) => { // if logged in via wallet-connect but chainId is not supported, // default to mainnet (ie. Use JSON-RPC provider) - return getIsValidChainId(supportedChains, walletConnectChainId) - ? walletProvider - : rpcUrl; + return getIsValidChainId(supportedChains, walletConnectChainId) ? walletProvider : rpcUrl; } // NOT logged in but has wallet installed (eg. Metamask). @@ -92,9 +86,7 @@ export const getProvider = (supportedChains: Chain[], rpcUrls: RpcUrl) => { const windowEthereum = getWindowEthereum(); if (windowEthereum?.chainId) { const walletChainId = Number(windowEthereum.chainId); - return getIsValidChainId(supportedChains, walletChainId) - ? windowEthereum - : rpcUrl; + return getIsValidChainId(supportedChains, walletChainId) ? windowEthereum : rpcUrl; } // fallback to mainnet JSON RPC provider @@ -105,10 +97,7 @@ export const getProvider = (supportedChains: Chain[], rpcUrls: RpcUrl) => { * gets ethers provider from the connected wallet or * installed wallet or fallback to mainnet */ -export const getEthersProvider = ( - supportedChains: Chain[], - rpcUrls: RpcUrl -) => { +export const getEthersProvider = (supportedChains: Chain[], rpcUrls: RpcUrl) => { const provider = getProvider(supportedChains, rpcUrls); // if provider is a string, it is a JSON-RPC provider @@ -116,7 +105,7 @@ export const getEthersProvider = ( return new ethers.JsonRpcProvider(provider); } - return new ethers.FallbackProvider([provider]); + return new ethers.BrowserProvider(provider); }; /** @@ -126,16 +115,14 @@ export const getEthersProvider = ( */ export const getChainIdOrDefaultToMainnet = ( SUPPORTED_CHAINS: Chain[], - chainIdPassed: string | number + chainIdPassed: string | number, ) => { if (!chainIdPassed) { throw new Error('chainId is not provided'); } const chain = Number(chainIdPassed); - return getIsValidChainId(SUPPORTED_CHAINS, chain) - ? chain - : SUPPORTED_CHAINS[0].id; + return getIsValidChainId(SUPPORTED_CHAINS, chain) ? chain : SUPPORTED_CHAINS[0].id; }; /** @@ -143,7 +130,7 @@ export const getChainIdOrDefaultToMainnet = ( */ export const getChainIdOrDefaultToFirstSupportedChain = ( SUPPORTED_CHAINS: Chain[], - chainIdPassed: string | number + chainIdPassed: string | number, ) => { return getChainIdOrDefaultToMainnet(SUPPORTED_CHAINS, chainIdPassed); }; @@ -152,10 +139,7 @@ export const getChainIdOrDefaultToFirstSupportedChain = ( * get chainId from the providers or fallback to default chainId (mainnet) * first element of supportedChains is the default chainId */ -export const getChainId = ( - supportedChains: Chain[], - chainId?: string | number | null -) => { +export const getChainId = (supportedChains: Chain[], chainId?: string | number | null) => { // if window is undefined, we are in server side // return undefined if (typeof window === 'undefined') { @@ -172,10 +156,7 @@ export const getChainId = ( const walletProvider = getModalProvider(); if (walletProvider?.chainId) { const walletConnectChainId = walletProvider.chainId; - return getChainIdOrDefaultToFirstSupportedChain( - supportedChains, - walletConnectChainId - ); + return getChainIdOrDefaultToFirstSupportedChain(supportedChains, walletConnectChainId); } // NOT logged in but has wallet installed (eg. metamask). @@ -183,10 +164,7 @@ export const getChainId = ( const windowEthereum = getWindowEthereum(); if (windowEthereum?.chainId) { const walletChainId = windowEthereum.chainId; - return getChainIdOrDefaultToFirstSupportedChain( - supportedChains, - walletChainId - ); + return getChainIdOrDefaultToFirstSupportedChain(supportedChains, walletChainId); } // has no wallet (eg. incognito mode or no wallet installed) diff --git a/libs/util-functions/src/lib/sendTransaction/index.ts b/libs/util-functions/src/lib/sendTransaction/index.ts index f297f09b..f087f72f 100644 --- a/libs/util-functions/src/lib/sendTransaction/index.ts +++ b/libs/util-functions/src/lib/sendTransaction/index.ts @@ -1,7 +1,8 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Contract } from 'ethers'; + import { notifyError, notifyWarning } from '../notifications'; import { getChainId, getEthersProvider, pollTransactionDetails } from './helpers'; -import { Chain, RpcUrl, Web3ReceiptType } from './types'; +import { Chain, RpcUrl } from './types'; export const SAFE_API_MAINNET = 'https://safe-transaction-mainnet.safe.global/api/v1/multisig-transactions'; @@ -36,7 +37,7 @@ export const getUrl = (hash: string, chainId: number) => { * poll until the hash has been approved */ export const sendTransaction = async ( - sendFn: any, + sendFn: Contract, account = (window as any)?.MODAL_PROVIDER?.accounts[0], { supportedChains, rpcUrls }: { supportedChains: Chain[]; rpcUrls: RpcUrl }, ) => { @@ -87,8 +88,7 @@ export const sendTransaction = async ( }); } else { // usual send function - const receipt: Web3ReceiptType = await sendFn(); - return receipt; + return sendFn; } } catch (e) { notifyError('Error occurred while sending transaction');