diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 668ede14..618bdd8c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -49,7 +49,7 @@ jobs: VITE_SENTRY_DSN_PROJECT_URI: ${{ secrets.SENTRY_DSN_PROJECT_URI }} VITE_SENTRY_DSN_PROJECT_ID: ${{ secrets.SENTRY_DSN_PROJECT_ID }} VITE_GITHUB_HASH: ${{ github.sha }} - VITE_IO_PROCESS_ID: ${{ secrets.IO_PROCESS_ID }} + VITE_ARIO_PROCESS_ID: ${{ vars.ARIO_PROCESS_ID }} # ao infra settings VITE_AO_CU_URL: ${{ vars.VITE_AO_CU_URL }} - name: Add CNAME Record diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 21b505ba..63b3d65a 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -46,7 +46,7 @@ jobs: VITE_SENTRY_DSN_PROJECT_URI: ${{ secrets.SENTRY_DSN_PROJECT_URI }} VITE_SENTRY_DSN_PROJECT_ID: ${{ secrets.SENTRY_DSN_PROJECT_ID }} VITE_GITHUB_HASH: ${{ github.sha }} - VITE_IO_PROCESS_ID: ${{ secrets.IO_PROCESS_ID }} + VITE_ARIO_PROCESS_ID: ${{ secrets.ARIO_PROCESS_ID }} VITE_AO_CU_URL: ${{ vars.VITE_AO_CU_URL }} # Disribute to Firebase @@ -84,7 +84,7 @@ jobs: VITE_SENTRY_DSN_PROJECT_URI: ${{ secrets.SENTRY_DSN_PROJECT_URI }} VITE_SENTRY_DSN_PROJECT_ID: ${{ secrets.SENTRY_DSN_PROJECT_ID }} VITE_GITHUB_HASH: ${{ github.sha }} - VITE_IO_PROCESS_ID: ${{ secrets.IO_PROCESS_ID }} + VITE_ARIO_PROCESS_ID: ${{ vars.ARIO_PROCESS_ID }} DEPLOY_ANT_PROCESS_ID: ${{ secrets.DEPLOY_ANT_PROCESS_ID }} DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} VITE_AO_CU_URL: ${{ vars.VITE_AO_CU_URL }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd273ce..ecda7b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,119 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.0] - 2024-12-20 + +### Added + +- Redelegate Stake: Users can now redelegate stake and pending withdrawals between gateways. Includes moving to/from operator stake and delegated stake. + Redelegation fees are assessed at 10% per redelegation performed since the last fee reset, up to 60%. Fees are reset when no redelegations are performed in the last 7 days. + +### Changed + +- Leave Network: text updated to 90-days for vaulted funds +- Staking: Staking and Withdrawal are now separate modals that are initiated from unique popup menu options + +### Fixed + +- Gateway Details: Restored "Leave" (when viewing own gateway) and "Stake" (when viewing other gateways) buttons + ## [1.6.0] - 2024-12-10 -### Added +### Added -* Gateway Details - * Added Operator Stake card showing operator stake and EAY, as well as manage stake button for updating operator stake. - * Added collapsible Pending Withdrawals card for viewing current withdrawals as well as managing - them (canceling a withdrawal or initiating an expedited withdrawal). Visible only to the gateway operator. - * Added collapsible Active Delegates card showing the list of active delegates for the gateway. +- Gateway Details + - Added Operator Stake card showing operator stake and EAY, as well as manage stake button for updating operator stake. + - Added collapsible Pending Withdrawals card for viewing current withdrawals as well as managing + them (canceling a withdrawal or initiating an expedited withdrawal). Visible only to the gateway operator. + - Added collapsible Active Delegates card showing the list of active delegates for the gateway. ## [1.5.0] - 2024-12-04 -### Added +### Added -* Profile button shows user's ArNS Primary Name (if available) or wallet address when logged in -* Download buttons added to Reports page and individual Report page -* Observers: Added epoch selector to view prescribed observers for previous epochs -* Gateway Details Page - * Reported On By card: text links to gateway for observer, report button links to report - * Reported On card: Report button shows in header that links to that report's page +- Profile button shows user's ArNS Primary Name (if available) or wallet address when logged in +- Download buttons added to Reports page and individual Report page +- Observers: Added epoch selector to view prescribed observers for previous epochs +- Gateway Details Page + - Reported On By card: text links to gateway for observer, report button links to report + - Reported On card: Report button shows in header that links to that report's page ### Updated -* Staking and Withdrawal modals updated to show Review page for user to confirm operation before processing -* Withdrawal Modal: Added option for Standard and Expedited Withdrawal -* Modal dialog styles refreshed -* Reward Share Ratio capped to 95% when joining network and updating gateway settings +- Staking and Withdrawal modals updated to show Review page for user to confirm operation before processing +- Withdrawal Modal: Added option for Standard and Expedited Withdrawal +- Modal dialog styles refreshed +- Reward Share Ratio capped to 95% when joining network and updating gateway settings ## [1.4.3] - 2024-11-27 ### Updated -* Settings updated for staking: - * Staking withdrawals are now 90 days - * Gateway Operator Stake minimum is now 10,000 IO - * Minimum Delegated Staking amount for gateway configuration is now 10 IO +- Settings updated for staking: + - Staking withdrawals are now 90 days + - Gateway Operator Stake minimum is now 10,000 IO + - Minimum Delegated Staking amount for gateway configuration is now 10 IO ## [1.4.2] - 2024-11-20 ### Updated -* Show error message toast if the application is unable to retrieve the current epoch +- Show error message toast if the application is unable to retrieve the current epoch ## [1.4.1] - 2024-11-18 ### Updated -* Optimized loading of user stakes and pending withdrawals. +- Optimized loading of user stakes and pending withdrawals. ### Fixed -* Gateways count in site header should only count active gateways. +- Gateways count in site header should only count active gateways. ## [1.4.0] - 2024-11-14 ### Added -* View Pending Withdrawals on Staking page and support cancelling pending withdrawals as well as performing expedited withdrawals -* View Changelog in app by clicking version number in sidebar +- View Pending Withdrawals on Staking page and support cancelling pending withdrawals as well as performing expedited withdrawals +- View Changelog in app by clicking version number in sidebar ### Updated -* Staking page top cards now show balance, amount staking + pending withdrawals, and rewards earned last 14 epochs and last epoch +- Staking page top cards now show balance, amount staking + pending withdrawals, and rewards earned last 14 epochs and last epoch ### Changed -* Updated header style of cards -* Observations: Updated to use arweave.net for reference domain when generating observation report -* Observe: Default to using prescribed names +- Updated header style of cards +- Observations: Updated to use arweave.net for reference domain when generating observation report +- Observe: Default to using prescribed names ## [1.3.0] - 2024-10-21 ### Added -* New Dashboard home page that visualizes data for the state of the gateway network +- New Dashboard home page that visualizes data for the state of the gateway network ## [1.2.0] - 2024-10-17 ### Added -* “Reported On” and “Reported On By” cards on Gateway Details page for viewing observation status by epoch for a gateway -* “Software” card on gateway details page that shows gateway software version and available bundlers (if gateway has listed them) +- “Reported On” and “Reported On By” cards on Gateway Details page for viewing observation status by epoch for a gateway +- “Software” card on gateway details page that shows gateway software version and available bundlers (if gateway has listed them) ### Changed -* Updated Gateway Details page for leaving gateways to hide non-relevant cards and show leave date - +- Updated Gateway Details page for leaving gateways to hide non-relevant cards and show leave date ## [1.1.0] - 2024-10-08 ### Added -* Gateways > Reports: Add “AR.IOEpoch #” Column -* Gateways>Reports>Individual Reports - * Add Epoch # - * Remove Epoch start height -* Implemented Leave Network Flow: - * Adds button to Gateway Detail page to leave network when gateway shown is the user’s own gateway - * Hitting Leave shows a modal with information. User has to type “LEAVE NETWORK” before Leave Network button is enabled. - * Hitting Leave Network button initiates signature request and then a success message. - * Site is refreshed after leaving. -* Release version shown on sidebar - +- Gateways > Reports: Add “AR.IOEpoch #” Column +- Gateways>Reports>Individual Reports + - Add Epoch # + - Remove Epoch start height +- Implemented Leave Network Flow: + - Adds button to Gateway Detail page to leave network when gateway shown is the user’s own gateway + - Hitting Leave shows a modal with information. User has to type “LEAVE NETWORK” before Leave Network button is enabled. + - Hitting Leave Network button initiates signature request and then a success message. + - Site is refreshed after leaving. +- Release version shown on sidebar ### Changed -* Gateway Details: rename “Reward Ratios” to “Performance Ratios” -* Gateway Details: Fixes text bubble cut off when copying wallet address +- Gateway Details: rename “Reward Ratios” to “Performance Ratios” +- Gateway Details: Fixes text bubble cut off when copying wallet address ### Fixed -* Gateway Details: Remove Edit and Stake Buttons from gateways that are leaving +- Gateway Details: Remove Edit and Stake Buttons from gateways that are leaving ## [1.0.0] -* Initial versions of application; version was bumped to 1.1.0 for first public versioned release. +- Initial versions of application; version was bumped to 1.1.0 for first public versioned release. diff --git a/package.json b/package.json index 1a5bfbe4..0bac808b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ar-io/network-portal", "private": true, - "version": "1.6.0", + "version": "1.7.0", "type": "module", "scripts": { "build": "yarn clean && tsc --build tsconfig.build.json && NODE_OPTIONS=--max-old-space-size=32768 vite build", @@ -20,9 +20,10 @@ "deploy": "yarn build && permaweb-deploy --ant-process ${DEPLOY_ANT_PROCESS_ID}" }, "dependencies": { - "@ar.io/sdk": "2.7.0-alpha.5", + "@ar.io/sdk": "3.0.1-alpha.1", "@fontsource/rubik": "^5.0.19", "@headlessui/react": "^2.2.0", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-tooltip": "^1.0.7", "@sentry/browser": "^7.101.1", "@sentry/react": "^7.101.1", diff --git a/src/components/GatewaySelector.tsx b/src/components/GatewaySelector.tsx new file mode 100644 index 00000000..739ecab6 --- /dev/null +++ b/src/components/GatewaySelector.tsx @@ -0,0 +1,210 @@ +import { AoGatewayWithAddress, mARIOToken } from '@ar.io/sdk/web'; +import { EAY_TOOLTIP_FORMULA, EAY_TOOLTIP_TEXT } from '@src/constants'; +import useGateways from '@src/hooks/useGateways'; +import useProtocolBalance from '@src/hooks/useProtocolBalance'; +import { useGlobalState } from '@src/store'; +import { formatAddress, formatPercentage, formatWithCommas } from '@src/utils'; +import { calculateGatewayRewards } from '@src/utils/rewards'; +import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; +import { MathJax } from 'better-react-mathjax'; +import { InfoIcon, SearchIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import Button, { ButtonType } from './Button'; +import BaseModal from './modals/BaseModal'; +import TableView from './TableView'; +import Tooltip from './Tooltip'; + +export type GatewaySelectorProps = { + selectedGateway?: AoGatewayWithAddress; + setSelectedGateway: (gateway: AoGatewayWithAddress) => void; + gateways?: AoGatewayWithAddress[]; +}; + +interface TableData { + label: string; + gateway: AoGatewayWithAddress; + rewardShareRatio: number; + totalStake: number; + eay: number; +} + +const columnHelper = createColumnHelper(); + +const GatewaySelectorModal = ({ + gateways, + onClose, + onGatewaySelected, +}: { + gateways: AoGatewayWithAddress[]; + onClose: () => void; + onGatewaySelected: (gateway: AoGatewayWithAddress) => void; +}) => { + const ticker = useGlobalState((state) => state.ticker); + const [tableData, setTableData] = useState([]); + + const { data: prototocolBalance } = useProtocolBalance(); + const { data: totalGateways } = useGateways(); + + const [searchText, setSearchText] = useState(); + + useEffect(() => { + if (prototocolBalance && totalGateways && gateways) { + const tableData: TableData[] = gateways.map((gateway) => { + return { + gateway, + label: gateway.settings.label, + rewardShareRatio: gateway.settings.delegateRewardShareRatio, + totalStake: gateway.totalDelegatedStake, + eay: calculateGatewayRewards( + new mARIOToken(prototocolBalance).toARIO(), + Object.values(totalGateways).filter((g) => g.status == 'joined') + .length, + gateway, + ).EAY, + }; + }); + if (searchText && searchText.length > 0) { + const filteredData = tableData.filter((data) => { + return ( + data.label.toLowerCase().includes(searchText.toLowerCase()) || + data.gateway.settings.fqdn + .toLowerCase() + .includes(searchText.toLowerCase()) || + data.gateway.gatewayAddress + .toLowerCase() + .includes(searchText.toLowerCase()) + ); + }); + setTableData(filteredData); + } else { + setTableData(tableData); + } + } + }, [totalGateways, gateways, prototocolBalance, searchText]); + + // Define columns for the table + const columns: ColumnDef[] = [ + columnHelper.accessor('label', { + id: 'label', + header: 'Gateway', + sortDescFirst: true, + cell: ({ row }) => ( +
+
{row.original.label}
+
+ {formatAddress(row.original.gateway.gatewayAddress)} +
+
+ ), + }), + columnHelper.accessor('rewardShareRatio', { + id: 'rewardShareRatioe', + header: 'Reward Share', + sortDescFirst: true, + cell: ({ row }) => formatPercentage(row.original.rewardShareRatio / 100), + }), + columnHelper.accessor('totalStake', { + id: 'totalStake', + header: 'Total Stake', + sortDescFirst: true, + cell: ({ row }) => + `${formatWithCommas(row.original.totalStake)} ${ticker}`, + }), + columnHelper.accessor('eay', { + id: 'eay', + header: () => ( +
+ EAY + +

{EAY_TOOLTIP_TEXT}

+ {EAY_TOOLTIP_FORMULA} +
+ } + > + + + + ), + sortDescFirst: true, + cell: ({ row }) => ( +
+ {row.original.eay < 0 + ? 'N/A' + : `${formatWithCommas(row.original.eay * 100)}%`} +
+ ), + }), + ]; + + return ( + +
+
+ + setSearchText(e.target.value)} + /> +
+ { + onGatewaySelected(row.gateway); + onClose(); + }} + defaultSortingState={{ id: 'label', desc: false }} + isLoading={false} + noDataFoundText="No gateways found." + shortTable={true} + /> +
+
+ ); +}; + +const GatewaySelector = ({ + selectedGateway, + setSelectedGateway, + gateways, +}: GatewaySelectorProps) => { + const [showGatewaySelectorTable, setShowGatewaySelectorTable] = + useState(false); + + return ( +
+
+ {!gateways + ? 'Loading Gateways...' + : selectedGateway + ? formatAddress(selectedGateway.gatewayAddress) + : ''} +
+ +
+
+ {showGatewaySelectorTable && gateways && ( + setShowGatewaySelectorTable(false)} + onGatewaySelected={setSelectedGateway} + /> + )} +
+ ); +}; + +export default GatewaySelector; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8a0b0729..c60cd7c0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,4 +1,4 @@ -import { mIOToken } from '@ar.io/sdk/web'; +import { mARIOToken } from '@ar.io/sdk/web'; import { NBSP } from '@src/constants'; import useEpochCountdown from '@src/hooks/useEpochCountdown'; import useGateways from '@src/hooks/useGateways'; @@ -88,7 +88,7 @@ const Header = () => { value={ protocolBalance ? (
- {formatWithCommas(new mIOToken(protocolBalance).toIO().valueOf())}{' '} + {formatWithCommas(new mARIOToken(protocolBalance).toARIO().valueOf())}{' '} {ticker}
) : undefined diff --git a/src/components/WalletProvider.tsx b/src/components/WalletProvider.tsx index 0335dc9a..b096d12b 100644 --- a/src/components/WalletProvider.tsx +++ b/src/components/WalletProvider.tsx @@ -1,6 +1,6 @@ -import { AOProcess, IO } from '@ar.io/sdk/web'; +import { AOProcess, ARIO } from '@ar.io/sdk/web'; import { connect } from '@permaweb/aoconnect'; -import { AO_CU_URL, IO_PROCESS_ID } from '@src/constants'; +import { AO_CU_URL, ARIO_PROCESS_ID } from '@src/constants'; import { useEffectOnce } from '@src/hooks/useEffectOnce'; import { ArConnectWalletConnector } from '@src/services/wallets/ArConnectWalletConnector'; import { useGlobalState } from '@src/store'; @@ -39,10 +39,10 @@ const WalletProvider = ({ children }: { children: ReactElement }) => { const signer = wallet.signer; if (signer) { - const writeable = IO.init({ + const writeable = ARIO.init({ signer, process: new AOProcess({ - processId: IO_PROCESS_ID.toString(), + processId: ARIO_PROCESS_ID.toString(), ao: connect({ CU_URL: AO_CU_URL, }), diff --git a/src/components/forms/validation.ts b/src/components/forms/validation.ts index 2e7bf3cf..64697960 100644 --- a/src/components/forms/validation.ts +++ b/src/components/forms/validation.ts @@ -46,7 +46,7 @@ export const validateTransactionId = ( }; }; -export const validateIOAmount = ( +export const validateARIOAmount = ( propertyName: string, ticker: string, min: number, diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index a881fc18..9aabb29a 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -47,6 +47,7 @@ import StatsArrowIcon from './stats_arrow.svg?react'; import StreakDownArrowIcon from './streak_down_arrow.svg?react'; import StreakUpArrowIcon from './streak_up_arrow.svg?react'; import SuccessCheck from './success_check.svg?react'; +import ThreeDotsIcon from './three_dots_icon.svg?react'; import TimerIcon from './timer.svg?react'; import ToastCloseIcon from './toast_close.svg?react'; import WalletIcon from './wallet.svg?react'; @@ -102,6 +103,7 @@ export { StreakDownArrowIcon, StreakUpArrowIcon, SuccessCheck, + ThreeDotsIcon, TimerIcon, ToastCloseIcon, WalletIcon, diff --git a/src/components/icons/three_dots_icon.svg b/src/components/icons/three_dots_icon.svg new file mode 100644 index 00000000..8bb10bf6 --- /dev/null +++ b/src/components/icons/three_dots_icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/modals/BaseModal.tsx b/src/components/modals/BaseModal.tsx index 55fe3389..954a69e6 100644 --- a/src/components/modals/BaseModal.tsx +++ b/src/components/modals/BaseModal.tsx @@ -7,14 +7,20 @@ const BaseModal = ({ children, showCloseButton = true, useDefaultPadding = true, + closeOnClickOutside = false, }: { onClose: () => void; children: ReactElement; showCloseButton?: boolean; useDefaultPadding?: boolean; + closeOnClickOutside?: boolean; }) => { return ( - {}} className="relative z-10"> + {}} + className="relative z-10" + > } className={`w-full ${!termsAccepted && 'pointer-events-none opacity-30'}`} /> diff --git a/src/components/modals/InstantWithdrawalModal.tsx b/src/components/modals/InstantWithdrawalModal.tsx index 76d50b1b..3c029ee1 100644 --- a/src/components/modals/InstantWithdrawalModal.tsx +++ b/src/components/modals/InstantWithdrawalModal.tsx @@ -1,4 +1,4 @@ -import { AoGateway, AoVaultData, mIOToken } from '@ar.io/sdk/web'; +import { AoGateway, AoVaultData, mARIOToken } from '@ar.io/sdk/web'; import { WRITE_OPTIONS } from '@src/constants'; import { useGlobalState } from '@src/store'; import { formatAddress, formatDateTime, formatWithCommas } from '@src/utils'; @@ -54,8 +54,8 @@ const InstantWithdrawalModal = ({ setCalculatedFeeAndAmountReturning({ penaltyRate, - fee: new mIOToken(fee).toIO().valueOf(), - amountReturning: new mIOToken(amountReturning).toIO().valueOf(), + fee: new mARIOToken(fee).toARIO().valueOf(), + amountReturning: new mARIOToken(amountReturning).toARIO().valueOf(), }); }, [setCalculatedFeeAndAmountReturning, vault]); @@ -149,7 +149,7 @@ const InstantWithdrawalModal = ({
Withdraw
} className={`w-full ${!termsAccepted && 'pointer-events-none opacity-30'}`} /> diff --git a/src/components/modals/LeaveNetworkModal.tsx b/src/components/modals/LeaveNetworkModal.tsx index 557d07a2..66673644 100644 --- a/src/components/modals/LeaveNetworkModal.tsx +++ b/src/components/modals/LeaveNetworkModal.tsx @@ -8,9 +8,7 @@ import { LinkArrowIcon } from '../icons'; import BaseModal from './BaseModal'; import BlockingMessageModal from './BlockingMessageModal'; import SuccessModal from './SuccessModal'; -import { WRITE_OPTIONS } from '@src/constants'; - -const GATEWAY_OPERATOR_STAKE_MINIMUM = 50000; +import { GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO, WRITE_OPTIONS } from '@src/constants'; const LeaveNetworkModal = ({ onClose }: { onClose: () => void }) => { const queryClient = useQueryClient(); @@ -75,17 +73,17 @@ const LeaveNetworkModal = ({ onClose }: { onClose: () => void }) => {
  • Your gateway's primary stake ( - {formatWithCommas(GATEWAY_OPERATOR_STAKE_MINIMUM)} {ticker}) + {formatWithCommas(GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO)} {ticker}) will be vaulted and subject to a 90-day withdrawal period.
  • Any additional operator stake above the minimum ( - {formatWithCommas(GATEWAY_OPERATOR_STAKE_MINIMUM)} {ticker}) - will be vaulted and subject to a 30-day withdrawal period. + {formatWithCommas(GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO)} {ticker}) + will be vaulted and subject to a 90-day withdrawal period.
  • Any existing delegated stakes will be vaulted and subject to - 30-day withdrawal period.{' '} + 90-day withdrawal period.{' '}
  • Your gateway status will change to leaving and will no longer be diff --git a/src/components/modals/OperatorStakingModal.tsx b/src/components/modals/OperatorStakingModal.tsx index dc58fd44..bd67e63d 100644 --- a/src/components/modals/OperatorStakingModal.tsx +++ b/src/components/modals/OperatorStakingModal.tsx @@ -1,27 +1,21 @@ -import { AoGatewayWithAddress, IOToken, mIOToken } from '@ar.io/sdk/web'; -import { Label, Radio, RadioGroup } from '@headlessui/react'; +import { AoGatewayWithAddress, ARIOToken, mARIOToken } from '@ar.io/sdk/web'; import { EAY_TOOLTIP_FORMULA, EAY_TOOLTIP_TEXT } from '@src/constants'; import useBalances from '@src/hooks/useBalances'; import useGateways from '@src/hooks/useGateways'; import useProtocolBalance from '@src/hooks/useProtocolBalance'; import { useGlobalState } from '@src/store'; -import { WithdrawalType } from '@src/types'; import { formatAddress, formatPercentage, formatWithCommas } from '@src/utils'; import { calculateOperatorRewards } from '@src/utils/rewards'; import { MathJax } from 'better-react-mathjax'; -import { useCallback, useEffect, useState, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Button, { ButtonType } from '../Button'; import LabelValueRow from '../LabelValueRow'; import Tooltip from '../Tooltip'; import ErrorMessageIcon from '../forms/ErrorMessageIcon'; -import { - validateIOAmount, - validateOperatorWithdrawAmount, -} from '../forms/validation'; -import { CircleCheckIcon, CircleIcon, InfoIcon } from '../icons'; +import { validateARIOAmount as validateARIOAmount } from '../forms/validation'; +import { InfoIcon } from '../icons'; import BaseModal from './BaseModal'; import ReviewStakeModal from './ReviewStakeModal'; -import ReviewWithdrawalModal from './ReviewWithdrawalModal'; const OperatorStakingModal = ({ onClose, @@ -37,17 +31,10 @@ const OperatorStakingModal = ({ const { data: gateways } = useGateways(); const ticker = useGlobalState((state) => state.ticker); - const [tab, setTab] = useState(0); - const [currentStake, setCurrentStake] = useState(0); const [amountToStake, setAmountToStake] = useState(''); - const [amountToWithdraw, setAmountToWithdraw] = useState(''); - const [withdrawalType, setWithdrawalType] = - useState('standard'); const [showReviewStakeModal, setShowReviewStakeModal] = useState(false); - const [showReviewWithdrawalModal, setShowReviewWithdrawalModal] = - useState(false); const [EAY, setEAY] = useState('-'); @@ -55,54 +42,35 @@ const OperatorStakingModal = ({ if (!gateway) { return; } - setCurrentStake(new mIOToken(gateway.operatorStake).toIO().valueOf()); + setCurrentStake(new mARIOToken(gateway.operatorStake).toARIO().valueOf()); }, [gateway]); - const newTotalStake = - tab == 0 - ? currentStake + parseFloat(amountToStake) - : currentStake - parseFloat(amountToWithdraw); + const newTotalStake = currentStake + parseFloat(amountToStake); const minDelegatedStake = gateway - ? new mIOToken(gateway?.settings.minDelegatedStake).toIO().valueOf() + ? new mARIOToken(gateway?.settings.minDelegatedStake).toARIO().valueOf() : 10; const minRequiredStakeToAdd = currentStake > 0 ? 1 : minDelegatedStake; - const withdrawalFee = - withdrawalType === 'expedited' ? 0.5 * parseFloat(amountToWithdraw) : 0; - const returningAmount = isNaN(parseFloat(amountToWithdraw)) - ? '-' - : +( - isNaN(withdrawalFee) - ? parseFloat(amountToWithdraw) - : parseFloat(amountToWithdraw) - withdrawalFee - ).toFixed(4); - - const validators = useMemo(() => ({ - stakeAmount: validateIOAmount('Stake Amount', ticker, 1, balances?.io), - withdrawAmount: validateOperatorWithdrawAmount( - 'Withdraw Amount', - ticker, - currentStake, - ), - }), [ticker, balances?.io, currentStake]); + const validators = useMemo( + () => ({ + stakeAmount: validateARIOAmount('Stake Amount', ticker, 1, balances?.io), + }), + [ticker, balances?.io], + ); const isFormValid = useCallback(() => { - if (tab == 0) { - return validators.stakeAmount(amountToStake) == undefined; - } else { - return validators.withdrawAmount(amountToWithdraw) == undefined; - } - }, [tab, amountToStake, amountToWithdraw, validators]); + return validators.stakeAmount(amountToStake) == undefined; + }, [amountToStake, validators]); useEffect(() => { - if (tab == 0 && protocolBalance && gateways && gateway && isFormValid()) { - const newTotalStake = currentStake + parseFloat(amountToStake) + if (protocolBalance && gateways && gateway && isFormValid()) { + const newTotalStake = currentStake + parseFloat(amountToStake); const { EAY } = calculateOperatorRewards( - new mIOToken(protocolBalance).toIO(), + new mARIOToken(protocolBalance).toARIO(), Object.values(gateways).filter((g) => g.status == 'joined').length, gateway, - new IOToken(newTotalStake), + new ARIOToken(newTotalStake), ); setEAY(formatPercentage(EAY)); } else { @@ -110,8 +78,6 @@ const OperatorStakingModal = ({ } }, [ amountToStake, - amountToWithdraw, - tab, gateway, protocolBalance, gateways, @@ -125,31 +91,14 @@ const OperatorStakingModal = ({ const remainingBalance = balances && parsedStake <= balances.io ? balances.io - parsedStake : -1; - const parsedWithdrawing = parseFloat( - amountToWithdraw.length === 0 ? '0' : amountToWithdraw, - ); - const remainingWithdrawalBalance = currentStake - 10000 - parsedWithdrawing; - - const baseTabClassName = 'text-center py-3'; - const selectedTabClassNames = `${baseTabClassName} bg-grey-700 border-b border-red-400`; - const nonSelectedTabClassNames = `${baseTabClassName} bg-grey-1000 text-low`; - const setMaxAmount = () => { - if (tab == 0) { - setAmountToStake((balances?.io || 0) + ''); - } else { - setAmountToWithdraw(currentStake + ''); - } + setAmountToStake((balances?.io || 0) + ''); }; - const disableInput = - !gateway || - (tab == 0 && (balances?.io || 0) < minRequiredStakeToAdd) || - (tab == 1 && currentStake <= 0); + const disableInput = !gateway || (balances?.io || 0) < minRequiredStakeToAdd; const errorMessages = { stakeAmount: validators.stakeAmount(amountToStake), - withdrawAmount: validators.withdrawAmount(amountToWithdraw), cannotStake: (balances?.io || 0) < minRequiredStakeToAdd ? `Insufficient balance, at least ${minRequiredStakeToAdd} IO required.` @@ -159,19 +108,10 @@ const OperatorStakingModal = ({ return (
    -
    - - +
    +
    + Stake +
    @@ -196,10 +136,8 @@ const OperatorStakingModal = ({
    Amount:
    - {tab == 0 - ? balances && - `Available: ${remainingBalance >= 0 ? formatWithCommas(+remainingBalance) : '-'} ${ticker}` - : `Available to Withdraw: ${remainingWithdrawalBalance >= 0 ? formatWithCommas(remainingWithdrawalBalance) : '-'} ${ticker}`} + {balances && + `Available: ${remainingBalance >= 0 ? formatWithCommas(+remainingBalance) : '-'} ${ticker}`}
    @@ -210,8 +148,8 @@ const OperatorStakingModal = ({ disabled={disableInput} readOnly={disableInput} type="text" - placeholder={`Enter amount of ${ticker} to ${tab == 0 ? 'stake' : 'withdraw'}`} - value={tab == 0 ? amountToStake : amountToWithdraw} + placeholder={`Enter amount of ${ticker} to stake`} + value={amountToStake} onChange={(e) => { const textValue = e.target.value; @@ -219,15 +157,10 @@ const OperatorStakingModal = ({ return; } - if (tab == 0) { - setAmountToStake(textValue); - } else { - setAmountToWithdraw(textValue); - } + setAmountToStake(textValue); }} - > - {tab == 0 && - gateway && + /> + {gateway && (amountToStake?.length > 0 || (balances?.io || 0) < minRequiredStakeToAdd) && (errorMessages.cannotStake || errorMessages.stakeAmount) && ( @@ -238,14 +171,6 @@ const OperatorStakingModal = ({ tooltipPadding={'3'} /> )} - {tab == 1 && - amountToWithdraw?.length > 0 && - errorMessages.withdrawAmount && ( - - )}
    -
    - {tab == 1 && ( - setWithdrawalType(v)} - > - -
    -
    - - - -
    -

    - 30 day withdrawal period with no fees. -

    -
    -
    - - -
    -
    - - - -
    -

    - Instant withdrawal with 50% fee. -

    -
    -
    -
    - )} -
    +
    - {tab == 1 && withdrawalType == 'expedited' && ( - <> - - - - )} -
    - {tab == 0 && ( - - )} + - {tab == 0 && ( - -

    {EAY_TOOLTIP_TEXT}

    - - {EAY_TOOLTIP_FORMULA} - -
    - } - > - - - } - /> - )} + +

    {EAY_TOOLTIP_TEXT}

    + {EAY_TOOLTIP_FORMULA} +
    + } + > + + + } + />
    { - tab == 0 - ? setShowReviewStakeModal(true) - : setShowReviewWithdrawalModal(true); + setShowReviewStakeModal(true); }} buttonType={ButtonType.PRIMARY} title="Review" @@ -379,19 +239,6 @@ const OperatorStakingModal = ({ walletAddress={walletAddress} /> )} - {showReviewWithdrawalModal && gateway && walletAddress && ( - setShowReviewWithdrawalModal(false)} - onSuccess={() => onClose()} - ticker={ticker} - walletAddress={walletAddress} - withdrawalFee={withdrawalFee} - returningAmount={returningAmount} - /> - )}
    ); diff --git a/src/components/modals/OperatorWithdrawalModal.tsx b/src/components/modals/OperatorWithdrawalModal.tsx new file mode 100644 index 00000000..c28af7d6 --- /dev/null +++ b/src/components/modals/OperatorWithdrawalModal.tsx @@ -0,0 +1,264 @@ +import { AoGatewayWithAddress, mARIOToken } from '@ar.io/sdk/web'; +import { Label, Radio, RadioGroup } from '@headlessui/react'; +import useBalances from '@src/hooks/useBalances'; +import { useGlobalState } from '@src/store'; +import { WithdrawalType } from '@src/types'; +import { formatAddress, formatWithCommas } from '@src/utils'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Button, { ButtonType } from '../Button'; +import LabelValueRow from '../LabelValueRow'; +import ErrorMessageIcon from '../forms/ErrorMessageIcon'; +import { validateOperatorWithdrawAmount } from '../forms/validation'; +import { CircleCheckIcon, CircleIcon } from '../icons'; +import BaseModal from './BaseModal'; +import ReviewWithdrawalModal from './ReviewWithdrawalModal'; + +const OperatorStakingModal = ({ + onClose, + gateway, +}: { + open: boolean; + onClose: () => void; + gateway: AoGatewayWithAddress; +}) => { + const walletAddress = useGlobalState((state) => state.walletAddress); + const { data: balances } = useBalances(walletAddress); + const ticker = useGlobalState((state) => state.ticker); + + const [currentStake, setCurrentStake] = useState(0); + const [amountToWithdraw, setAmountToWithdraw] = useState(''); + const [withdrawalType, setWithdrawalType] = + useState('standard'); + + const [showReviewWithdrawalModal, setShowReviewWithdrawalModal] = + useState(false); + + useEffect(() => { + if (!gateway) { + return; + } + setCurrentStake(new mARIOToken(gateway.operatorStake).toARIO().valueOf()); + }, [gateway]); + + const newTotalStake = currentStake - parseFloat(amountToWithdraw); + + const minDelegatedStake = gateway + ? new mARIOToken(gateway?.settings.minDelegatedStake).toARIO().valueOf() + : 10; + const minRequiredStakeToAdd = currentStake > 0 ? 1 : minDelegatedStake; + + const withdrawalFee = + withdrawalType === 'expedited' ? 0.5 * parseFloat(amountToWithdraw) : 0; + const returningAmount = isNaN(parseFloat(amountToWithdraw)) + ? '-' + : +( + isNaN(withdrawalFee) + ? parseFloat(amountToWithdraw) + : parseFloat(amountToWithdraw) - withdrawalFee + ).toFixed(4); + + const validators = useMemo( + () => ({ + withdrawAmount: validateOperatorWithdrawAmount( + 'Withdraw Amount', + ticker, + currentStake, + ), + }), + [ticker, currentStake], + ); + + const isFormValid = useCallback(() => { + return validators.withdrawAmount(amountToWithdraw) == undefined; + }, [amountToWithdraw, validators]); + + const parsedWithdrawing = parseFloat( + amountToWithdraw.length === 0 ? '0' : amountToWithdraw, + ); + const remainingWithdrawalBalance = currentStake - 10000 - parsedWithdrawing; + + const setMaxAmount = () => { + setAmountToWithdraw(currentStake + ''); + }; + + const disableInput = !gateway || currentStake <= 0; + + const errorMessages = { + withdrawAmount: validators.withdrawAmount(amountToWithdraw), + cannotStake: + (balances?.io || 0) < minRequiredStakeToAdd + ? `Insufficient balance, at least ${minRequiredStakeToAdd} IO required.` + : undefined, + }; + + return ( + +
    +
    +
    + Withdraw +
    +
    +
    +
    + + + + + +
    + +
    +
    Amount:
    +
    +
    + {`Available to Withdraw: ${remainingWithdrawalBalance >= 0 ? formatWithCommas(remainingWithdrawalBalance) : '-'} ${ticker}`} +
    +
    +
    + { + const textValue = e.target.value; + + if (textValue && isNaN(+e.target.value)) { + return; + } + + setAmountToWithdraw(textValue); + }} + /> + + {amountToWithdraw?.length > 0 && errorMessages.withdrawAmount && ( + + )} +
    +
    + setWithdrawalType(v)} + > + +
    +
    + + + +
    +

    + 90 day withdrawal period with no fees. +

    +
    +
    + + +
    +
    + + + +
    +

    + Instant withdrawal with 50% fee. +

    +
    +
    +
    +
    +
    +
    + {withdrawalType == 'expedited' && ( + <> + + + + )} + +
    + +
    +
    +
    +
    + {showReviewWithdrawalModal && gateway && walletAddress && ( + setShowReviewWithdrawalModal(false)} + onSuccess={() => onClose()} + ticker={ticker} + walletAddress={walletAddress} + withdrawalFee={withdrawalFee} + returningAmount={returningAmount} + /> + )} +
    +
    + ); +}; + +export default OperatorStakingModal; diff --git a/src/components/modals/RedelegateModal.tsx b/src/components/modals/RedelegateModal.tsx new file mode 100644 index 00000000..9473ef68 --- /dev/null +++ b/src/components/modals/RedelegateModal.tsx @@ -0,0 +1,350 @@ +import { AoGatewayWithAddress, ARIOToken, mARIOToken } from '@ar.io/sdk/web'; +import useDelegateStakes from '@src/hooks/useDelegateStakes'; +import useGateways from '@src/hooks/useGateways'; +import useRedelegationFee from '@src/hooks/useRedelegationFee'; +import { useGlobalState } from '@src/store'; +import { formatWithCommas } from '@src/utils'; +import { InfoIcon } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import Button, { ButtonType } from '../Button'; +import GatewaySelector from '../GatewaySelector'; +import LabelValueRow from '../LabelValueRow'; +import Tooltip from '../Tooltip'; +import ErrorMessageIcon from '../forms/ErrorMessageIcon'; +import { validateARIOAmount } from '../forms/validation'; +import BaseModal from './BaseModal'; +import ReviewRedelegateModal from './ReviewRedelegateModal'; +import { REDELEGATION_FEE_TOOLTIP_TEXT } from '@src/constants'; + +export type RedelegateModalProps = { + onClose: () => void; + sourceGateway: AoGatewayWithAddress; + vaultId?: string; + maxRedelegationStake: ARIOToken; +}; + +const RedelegateModal = ({ + onClose, + sourceGateway, + vaultId, + maxRedelegationStake, +}: RedelegateModalProps) => { + const walletAddress = useGlobalState((state) => state.walletAddress); + const ticker = useGlobalState((state) => state.ticker); + + const { data: delegateStakes } = useDelegateStakes(walletAddress?.toString()); + + const [targetGateway, setTargetGateway] = useState(); + const [targetGatewayCurrentStake, setTargetGatewayCurrentStake] = + useState(); + + const [amountToRedelegate, setAmountToRedelegate] = useState(''); + const [errorMessage, setErrorMessage] = useState(); + const [isFormValid, setIsFormValid] = useState(false); + + const [showReviewRedelegateModal, setShowReviewRedelegateModal] = + useState(false); + + const { data: redelegationFee } = useRedelegationFee( + walletAddress?.toString(), + ); + + const { data: gateways } = useGateways(); + const [filteredGateways, setFilteredGateways] = + useState(); + + useEffect(() => { + if (gateways) { + const filteredGateways = Object.entries(gateways) + .map(([address, gateway]) => { + return { ...gateway, gatewayAddress: address }; + }) + .filter( + (gateway) => + gateway.status === 'joined' && + gateway.settings.allowDelegatedStaking && + gateway.gatewayAddress !== sourceGateway.gatewayAddress, + ); + setFilteredGateways(filteredGateways); + } + }, [gateways, sourceGateway.gatewayAddress]); + + useEffect(() => { + if (targetGateway && walletAddress) { + if (targetGateway.gatewayAddress === walletAddress.toString()) { + setTargetGatewayCurrentStake( + new mARIOToken(targetGateway.operatorStake).toARIO().valueOf(), + ); + } else if (delegateStakes) { + const stake = delegateStakes.stakes.find( + (stake) => stake.gatewayAddress === targetGateway.gatewayAddress, + )?.balance; + setTargetGatewayCurrentStake( + new mARIOToken(stake ?? 0).toARIO().valueOf(), + ); + } + } + }, [targetGateway, delegateStakes, walletAddress]); + + const minDelegatedStake = targetGateway + ? new mARIOToken(targetGateway?.settings.minDelegatedStake) + .toARIO() + .valueOf() + : 10; + const minRequiredStakeToAdd = + (targetGatewayCurrentStake ?? 0) > 0 ? 1 : minDelegatedStake; + + const validators = useMemo( + () => ({ + redelegationAmount: validateARIOAmount( + 'Redelegation Amount', + ticker, + minRequiredStakeToAdd, + maxRedelegationStake.valueOf(), + ), + }), + [ticker, minRequiredStakeToAdd, maxRedelegationStake], + ); + + useEffect(() => { + if (!targetGateway) { + setIsFormValid(false); + return; + } + + const amount = parseFloat(amountToRedelegate); + const maxStake = maxRedelegationStake.valueOf(); + + const sourceMinStakeARIO = new mARIOToken( + sourceGateway.settings.minDelegatedStake, + ) + .toARIO() + .valueOf(); + + if ( + // checking if redelegation source is from a stake rather than a vault + vaultId === undefined && + amount != maxStake && + maxStake - amount < sourceMinStakeARIO + ) { + setErrorMessage( + `Amount to redelegate must either leave enough stake to meet the source gateway's minimum delegated stake (${formatWithCommas(sourceMinStakeARIO)} ${ticker}) or move the entire stake completely.`, + ); + setIsFormValid(false); + return; + } + + const redelegationAmountError = + validators.redelegationAmount(amountToRedelegate); + + if (redelegationAmountError !== undefined) { + setErrorMessage(redelegationAmountError); + setIsFormValid(false); + return; + } + + if (maxStake < minRequiredStakeToAdd) { + setErrorMessage( + `Insufficient redelegation balance, at least ${minRequiredStakeToAdd} IO required for target gateway.`, + ); + setIsFormValid(false); + return; + } + + setErrorMessage(undefined); + setIsFormValid(true); + }, [ + amountToRedelegate, + maxRedelegationStake, + minRequiredStakeToAdd, + sourceGateway.settings.minDelegatedStake, + targetGateway, + ticker, + validators, + vaultId, + ]); + + const fee = useMemo(() => { + if (redelegationFee && amountToRedelegate && isFormValid) { + const feeAmount = + (redelegationFee.redelegationFeeRate / 100) * + parseFloat(amountToRedelegate); + return feeAmount; + } + return 0; + }, [redelegationFee, amountToRedelegate, isFormValid]); + + const totalRedelegatedStake = parseFloat(amountToRedelegate) - fee; + const newTotalStake = + (targetGatewayCurrentStake ?? 0) + totalRedelegatedStake; + + const parsedStake = parseFloat( + amountToRedelegate.length === 0 ? '0' : amountToRedelegate, + ); + const remainingBalance = + parsedStake <= maxRedelegationStake.valueOf() + ? maxRedelegationStake.valueOf() - parsedStake + : -1; + + const setMaxAmount = () => { + setAmountToRedelegate(maxRedelegationStake + ''); + }; + + const disableInput = + !targetGateway || maxRedelegationStake.valueOf() < minRequiredStakeToAdd; + + return ( + +
    +
    +
    + Redelegate +
    +
    +
    +
    +
    + +
    + + + + + + +
    + +
    +
    Amount:
    +
    +
    + {`Available: ${ + remainingBalance >= 0 + ? formatWithCommas(+remainingBalance) + : '-' + } ${ticker}`} +
    +
    +
    + { + const textValue = e.target.value; + + if (textValue && isNaN(+e.target.value)) { + return; + } + + setAmountToRedelegate(textValue); + }} + /> + {targetGateway && + (amountToRedelegate?.length > 0 || + maxRedelegationStake.valueOf() < minRequiredStakeToAdd) && + errorMessage && ( + + )} +
    +
    +
    +
    + {fee > 0 && ( + +

    + {REDELEGATION_FEE_TOOLTIP_TEXT} +

    +
    + } + > + + + } + /> + )} + + + + +
    +
    +
    +
    + {showReviewRedelegateModal && targetGateway && walletAddress && ( + setShowReviewRedelegateModal(false)} + onSuccess={() => onClose()} + ticker={ticker} + walletAddress={walletAddress} + vaultId={vaultId} + /> + )} +
    +
    + ); +}; + +export default RedelegateModal; diff --git a/src/components/modals/ReviewRedelegateModal.tsx b/src/components/modals/ReviewRedelegateModal.tsx new file mode 100644 index 00000000..1daba32c --- /dev/null +++ b/src/components/modals/ReviewRedelegateModal.tsx @@ -0,0 +1,252 @@ +import { AoGatewayWithAddress, ARIOToken } from '@ar.io/sdk/web'; +import { + log, + REDELEGATION_FEE_TOOLTIP_TEXT, + WRITE_OPTIONS, +} from '@src/constants'; +import useRedelegationFee from '@src/hooks/useRedelegationFee'; +import { useGlobalState } from '@src/store'; +import { formatAddress, formatWithCommas } from '@src/utils'; +import { ArweaveTransactionID } from '@src/utils/ArweaveTransactionId'; +import { showErrorToast } from '@src/utils/toast'; +import { useQueryClient } from '@tanstack/react-query'; +import { InfoIcon } from 'lucide-react'; +import { useState } from 'react'; +import Button, { ButtonType } from '../Button'; +import { LinkArrowIcon } from '../icons'; +import LabelValueRow from '../LabelValueRow'; +import Tooltip from '../Tooltip'; +import BaseModal from './BaseModal'; +import BlockingMessageModal from './BlockingMessageModal'; +import SuccessModal from './SuccessModal'; +import WithdrawWarning from './WithdrawWarning'; + +type ReviewRedelegateModalProps = { + sourceGateway: AoGatewayWithAddress; + targetGateway: AoGatewayWithAddress; + amountToRedelegate: ARIOToken; + fee: number; + newTotalStake: number; + walletAddress: ArweaveTransactionID; + vaultId?: string; + onClose: () => void; + onSuccess: () => void; + ticker: string; +}; + +const ReviewRedelegateModal = ({ + sourceGateway, + targetGateway, + amountToRedelegate, + vaultId, + fee, + newTotalStake, + onSuccess, + onClose, + walletAddress, + ticker, +}: ReviewRedelegateModalProps) => { + const queryClient = useQueryClient(); + const arIOWriteableSDK = useGlobalState((state) => state.arIOWriteableSDK); + + const [txid, setTxid] = useState(); + + const [showBlockingMessageModal, setShowBlockingMessageModal] = + useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + + const { data: redelegationFee } = useRedelegationFee(); + + const totalRedelegatedStake = amountToRedelegate.valueOf() - fee; + + const submitForm = async () => { + if (arIOWriteableSDK) { + setShowBlockingMessageModal(true); + + try { + const { id: txID } = await arIOWriteableSDK.redelegateStake( + { + source: sourceGateway.gatewayAddress, + target: targetGateway.gatewayAddress, + stakeQty: amountToRedelegate.toMARIO(), + vaultId, + }, + WRITE_OPTIONS, + ); + + setTxid(txID); + + log.info(`Redelegate Stake txID: ${txID}`); + + queryClient.invalidateQueries({ + queryKey: ['gateway', walletAddress.toString()], + refetchType: 'all', + }); + queryClient.invalidateQueries({ + queryKey: ['gateways'], + refetchType: 'all', + }); + queryClient.invalidateQueries({ + queryKey: ['balances'], + refetchType: 'all', + }); + queryClient.invalidateQueries({ + queryKey: ['delegateStakes'], + refetchType: 'all', + }); + queryClient.invalidateQueries({ + queryKey: ['gatewayVaults'], + refetchType: 'all', + }); + + setShowSuccessModal(true); + } catch (e: any) { + showErrorToast(`${e}`); + } finally { + setShowBlockingMessageModal(false); + } + } + }; + + return ( + <> + +
    +
    + Review +
    +
    + + + + + + +
    + + + + + + + +
    + + + + {fee > 0 && ( + 0 && fee} ${redelegationFee ? `(-${redelegationFee.redelegationFeeRate}%)` : ''} ${ticker}`} + rightIcon={ + +

    {REDELEGATION_FEE_TOOLTIP_TEXT}

    +
    + } + > + + + } + /> + )} + + + + +
    + +
    + +
    + +
    + +
    + + +
    + + {showBlockingMessageModal && ( + setShowBlockingMessageModal(false)} + message="Sign the following data with your wallet to proceed." + > + )} + {showSuccessModal && ( + { + setShowSuccessModal(false); + onClose(); + onSuccess(); + }} + title="Congratulations" + bodyText={ +
    +
    You have successfully redelegated your stake.
    +
    +
    Transaction ID:
    + +
    +
    + } + /> + )} + + ); +}; + +export default ReviewRedelegateModal; diff --git a/src/components/modals/ReviewStakeModal.tsx b/src/components/modals/ReviewStakeModal.tsx index 8becf4e8..fb9bf1f1 100644 --- a/src/components/modals/ReviewStakeModal.tsx +++ b/src/components/modals/ReviewStakeModal.tsx @@ -1,4 +1,4 @@ -import { AoGatewayWithAddress, IOToken } from '@ar.io/sdk/web'; +import { AoGatewayWithAddress, ARIOToken } from '@ar.io/sdk/web'; import { log, WRITE_OPTIONS } from '@src/constants'; import { useGlobalState } from '@src/store'; import { formatAddress, formatWithCommas } from '@src/utils'; @@ -46,7 +46,7 @@ const ReviewStakeModal = ({ if(gateway.gatewayAddress === walletAddress.toString()) { const { id: txID } = await arIOWriteableSDK.increaseOperatorStake( { - increaseQty: new IOToken(amountToStake).toMIO(), + increaseQty: new ARIOToken(amountToStake).toMARIO(), }, WRITE_OPTIONS, ); @@ -58,7 +58,7 @@ const ReviewStakeModal = ({ const { id: txID } = await arIOWriteableSDK.delegateStake( { target: gateway.gatewayAddress, - stakeQty: new IOToken(amountToStake).toMIO(), + stakeQty: new ARIOToken(amountToStake).toMARIO(), }, WRITE_OPTIONS, ); diff --git a/src/components/modals/ReviewWithdrawalModal.tsx b/src/components/modals/ReviewWithdrawalModal.tsx index 63706d2a..7e319c08 100644 --- a/src/components/modals/ReviewWithdrawalModal.tsx +++ b/src/components/modals/ReviewWithdrawalModal.tsx @@ -1,4 +1,4 @@ -import { AoGatewayWithAddress, IOToken } from '@ar.io/sdk/web'; +import { AoGatewayWithAddress, ARIOToken } from '@ar.io/sdk/web'; import { log, WRITE_OPTIONS } from '@src/constants'; import { useGlobalState } from '@src/store'; import { WithdrawalType } from '@src/types'; @@ -69,7 +69,7 @@ const ReviewWithdrawalModal = ({ if (gateway.gatewayAddress === walletAddress.toString()) { const { id: txID } = await arIOWriteableSDK.decreaseOperatorStake( { - decreaseQty: new IOToken(amountToWithdraw).toMIO(), + decreaseQty: new ARIOToken(amountToWithdraw).toMARIO(), instant, }, WRITE_OPTIONS, @@ -81,7 +81,7 @@ const ReviewWithdrawalModal = ({ const { id: txID } = await arIOWriteableSDK.decreaseDelegateStake( { target: gateway.gatewayAddress, - decreaseQty: new IOToken(amountToWithdraw).toMIO(), + decreaseQty: new ARIOToken(amountToWithdraw).toMARIO(), instant, }, WRITE_OPTIONS, diff --git a/src/components/modals/StakeWithdrawalModal.tsx b/src/components/modals/StakeWithdrawalModal.tsx new file mode 100644 index 00000000..5c7d3c91 --- /dev/null +++ b/src/components/modals/StakeWithdrawalModal.tsx @@ -0,0 +1,280 @@ +import { mARIOToken } from '@ar.io/sdk/web'; +import { Label, Radio, RadioGroup } from '@headlessui/react'; +import useBalances from '@src/hooks/useBalances'; +import useDelegateStakes from '@src/hooks/useDelegateStakes'; +import useGateway from '@src/hooks/useGateway'; +import { useGlobalState } from '@src/store'; +import { WithdrawalType } from '@src/types'; +import { formatAddress, formatWithCommas } from '@src/utils'; +import { useEffect, useState } from 'react'; +import Button, { ButtonType } from '../Button'; +import LabelValueRow from '../LabelValueRow'; +import ErrorMessageIcon from '../forms/ErrorMessageIcon'; +import { validateWithdrawAmount } from '../forms/validation'; +import { CircleCheckIcon, CircleIcon } from '../icons'; +import BaseModal from './BaseModal'; +import ReviewWithdrawalModal from './ReviewWithdrawalModal'; + +const StakeWithdrawalModal = ({ + onClose, + ownerWallet, +}: { + open: boolean; + onClose: () => void; + ownerWallet: string; +}) => { + const walletAddress = useGlobalState((state) => state.walletAddress); + const { data: balances } = useBalances(walletAddress); + const ticker = useGlobalState((state) => state.ticker); + + const [currentStake, setCurrentStake] = useState(0); + const [amountToWithdraw, setAmountToWithdraw] = useState(''); + const [withdrawalType, setWithdrawalType] = + useState('standard'); + + const [showReviewWithdrawalModal, setShowReviewWithdrawalModal] = + useState(false); + + const { data: gateway } = useGateway({ + ownerWalletAddress: ownerWallet, + }); + + const { data: delegateStakes } = useDelegateStakes(walletAddress?.toString()); + + useEffect(() => { + if (!gateway || !delegateStakes) { + return; + } + const stake = delegateStakes.stakes.find( + (stake) => stake.gatewayAddress === gateway.gatewayAddress, + )?.balance; + setCurrentStake(new mARIOToken(stake ?? 0).toARIO().valueOf()); + }, [delegateStakes, gateway]); + + const allowDelegatedStaking = + gateway?.settings.allowDelegatedStaking ?? false; + + const minDelegatedStake = gateway + ? new mARIOToken(gateway?.settings.minDelegatedStake).toARIO().valueOf() + : 10; + const minRequiredStakeToAdd = currentStake > 0 ? 1 : minDelegatedStake; + + const withdrawalFee = + withdrawalType === 'expedited' ? 0.5 * parseFloat(amountToWithdraw) : 0; + const returningAmount = isNaN(parseFloat(amountToWithdraw)) + ? '-' + : +( + isNaN(withdrawalFee) + ? parseFloat(amountToWithdraw) + : parseFloat(amountToWithdraw) - withdrawalFee + ).toFixed(4); + + const validators = { + withdrawAmount: validateWithdrawAmount( + 'Withdraw Amount', + ticker, + currentStake, + minDelegatedStake, + ), + }; + + const isFormValid = () => { + if (!gateway) { + return false; + } + return validators.withdrawAmount(amountToWithdraw) == undefined; + }; + + const parsedWithdrawing = parseFloat( + amountToWithdraw.length === 0 ? '0' : amountToWithdraw, + ); + const remainingWithdrawalBalance = currentStake - parsedWithdrawing; + + const setMaxAmount = () => { + setAmountToWithdraw(currentStake + ''); + }; + + const disableInput = !gateway || currentStake <= 0; + + const errorMessages = { + withdrawAmount: validators.withdrawAmount(amountToWithdraw), + cannotStake: + (balances?.io || 0) < minRequiredStakeToAdd + ? `Insufficient balance, at least ${minRequiredStakeToAdd} IO required.` + : !allowDelegatedStaking + ? 'Gateway does not allow delegated staking.' + : undefined, + }; + + return ( + +
    +
    +
    + Withdraw +
    +
    +
    +
    + + + +
    + +
    +
    Amount:
    +
    +
    + {`Available to Withdraw: ${remainingWithdrawalBalance >= 0 ? formatWithCommas(remainingWithdrawalBalance) : '-'} ${ticker}`} +
    +
    +
    + { + const textValue = e.target.value; + + if (textValue && isNaN(+e.target.value)) { + return; + } + + setAmountToWithdraw(textValue); + }} + /> + {amountToWithdraw?.length > 0 && errorMessages.withdrawAmount && ( + + )} +
    +
    + setWithdrawalType(v)} + > + +
    +
    + + + +
    +

    + 90 day withdrawal period with no fees. +

    +
    +
    + + +
    +
    + + + +
    +

    + Instant withdrawal with 50% fee. +

    +
    +
    +
    +
    +
    +
    + {withdrawalType == 'expedited' && ( + <> + + + + )} + +
    + +
    +
    +
    +
    + {showReviewWithdrawalModal && gateway && walletAddress && ( + setShowReviewWithdrawalModal(false)} + onSuccess={() => onClose()} + ticker={ticker} + walletAddress={walletAddress} + withdrawalFee={withdrawalFee} + returningAmount={returningAmount} + /> + )} +
    +
    + ); +}; + +export default StakeWithdrawalModal; diff --git a/src/components/modals/StakingModal.tsx b/src/components/modals/StakingModal.tsx index c8c0287f..b5ca448e 100644 --- a/src/components/modals/StakingModal.tsx +++ b/src/components/modals/StakingModal.tsx @@ -1,12 +1,10 @@ -import { mIOToken } from '@ar.io/sdk/web'; -import { Label, Radio, RadioGroup } from '@headlessui/react'; +import { mARIOToken } from '@ar.io/sdk/web'; import { EAY_TOOLTIP_FORMULA, EAY_TOOLTIP_TEXT } from '@src/constants'; import useBalances from '@src/hooks/useBalances'; import useDelegateStakes from '@src/hooks/useDelegateStakes'; import useGateway from '@src/hooks/useGateway'; import useRewardsInfo from '@src/hooks/useRewardsInfo'; import { useGlobalState } from '@src/store'; -import { WithdrawalType } from '@src/types'; import { formatAddress, formatWithCommas } from '@src/utils'; import { MathJax } from 'better-react-mathjax'; import { useEffect, useState } from 'react'; @@ -14,15 +12,10 @@ import Button, { ButtonType } from '../Button'; import LabelValueRow from '../LabelValueRow'; import Tooltip from '../Tooltip'; import ErrorMessageIcon from '../forms/ErrorMessageIcon'; -import { - validateIOAmount, - validateWalletAddress, - validateWithdrawAmount, -} from '../forms/validation'; -import { CircleCheckIcon, CircleIcon, InfoIcon } from '../icons'; +import { validateARIOAmount, validateWalletAddress } from '../forms/validation'; +import { InfoIcon } from '../icons'; import BaseModal from './BaseModal'; import ReviewStakeModal from './ReviewStakeModal'; -import ReviewWithdrawalModal from './ReviewWithdrawalModal'; const StakingModal = ({ onClose, @@ -30,31 +23,19 @@ const StakingModal = ({ }: { open: boolean; onClose: () => void; - ownerWallet?: string; + ownerWallet: string; }) => { const walletAddress = useGlobalState((state) => state.walletAddress); const { data: balances } = useBalances(walletAddress); const ticker = useGlobalState((state) => state.ticker); - const [tab, setTab] = useState(0); - const [userEnteredWalletAddress, setUserEnteredWalletAddress] = - useState(''); - const [currentStake, setCurrentStake] = useState(0); const [amountToStake, setAmountToStake] = useState(''); - const [amountToWithdraw, setAmountToWithdraw] = useState(''); - const [withdrawalType, setWithdrawalType] = - useState('standard'); const [showReviewStakeModal, setShowReviewStakeModal] = useState(false); - const [showReviewWithdrawalModal, setShowReviewWithdrawalModal] = - useState(false); - - const gatewayOwnerWallet = - ownerWallet?.toString() ?? userEnteredWalletAddress; const { data: gateway } = useGateway({ - ownerWalletAddress: gatewayOwnerWallet, + ownerWalletAddress: ownerWallet, }); const { data: delegateStakes } = useDelegateStakes(walletAddress?.toString()); @@ -66,18 +47,14 @@ const StakingModal = ({ const stake = delegateStakes.stakes.find( (stake) => stake.gatewayAddress === gateway.gatewayAddress, )?.balance; - setCurrentStake(new mIOToken(stake ?? 0).toIO().valueOf()); + setCurrentStake(new mARIOToken(stake ?? 0).toARIO().valueOf()); }, [delegateStakes, gateway]); const allowDelegatedStaking = gateway?.settings.allowDelegatedStaking ?? false; - const newTotalStake = - tab == 0 - ? currentStake + parseFloat(amountToStake) - : currentStake - parseFloat(amountToWithdraw); - const newStake = - tab == 0 ? parseFloat(amountToStake) : -parseFloat(amountToWithdraw); + const newTotalStake = currentStake + parseFloat(amountToStake); + const newStake = parseFloat(amountToStake); const rewardsInfo = useRewardsInfo(gateway, newStake); const EAY = rewardsInfo && newTotalStake > 0 && !isNaN(rewardsInfo.EAY) @@ -87,45 +64,25 @@ const StakingModal = ({ : '-'; const minDelegatedStake = gateway - ? new mIOToken(gateway?.settings.minDelegatedStake).toIO().valueOf() + ? new mARIOToken(gateway?.settings.minDelegatedStake).toARIO().valueOf() : 10; const minRequiredStakeToAdd = currentStake > 0 ? 1 : minDelegatedStake; - const withdrawalFee = - withdrawalType === 'expedited' ? 0.5 * parseFloat(amountToWithdraw) : 0; - const returningAmount = isNaN(parseFloat(amountToWithdraw)) - ? '-' - : +( - isNaN(withdrawalFee) - ? parseFloat(amountToWithdraw) - : parseFloat(amountToWithdraw) - withdrawalFee - ).toFixed(4); - const validators = { address: validateWalletAddress('Gateway Owner'), - stakeAmount: validateIOAmount( + stakeAmount: validateARIOAmount( 'Stake Amount', ticker, minRequiredStakeToAdd, balances?.io, ), - withdrawAmount: validateWithdrawAmount( - 'Withdraw Amount', - ticker, - currentStake, - minDelegatedStake, - ), }; const isFormValid = () => { - if (!gateway || (tab == 0 && !allowDelegatedStaking)) { + if (!gateway || !allowDelegatedStaking) { return false; } - if (tab == 0) { - return validators.stakeAmount(amountToStake) == undefined; - } else { - return validators.withdrawAmount(amountToWithdraw) == undefined; - } + return validators.stakeAmount(amountToStake) == undefined; }; const parsedStake = parseFloat( @@ -134,32 +91,17 @@ const StakingModal = ({ const remainingBalance = balances && parsedStake <= balances.io ? balances.io - parsedStake : -1; - const parsedWithdrawing = parseFloat(amountToWithdraw.length === 0 ? '0' : amountToWithdraw); - const remainingWithdrawalBalance = currentStake - parsedWithdrawing; - - const baseTabClassName = 'text-center py-3'; - const selectedTabClassNames = `${baseTabClassName} bg-grey-700 border-b border-red-400`; - const nonSelectedTabClassNames = `${baseTabClassName} bg-grey-1000 text-low`; - const setMaxAmount = () => { - if (tab == 0) { - setAmountToStake((balances?.io || 0) + ''); - } else { - setAmountToWithdraw(currentStake + ''); - } + setAmountToStake((balances?.io || 0) + ''); }; const disableInput = !gateway || - (tab == 0 && - ((balances?.io || 0) < minRequiredStakeToAdd || - !allowDelegatedStaking)) || - (tab == 1 && currentStake <= 0); + (balances?.io || 0) < minRequiredStakeToAdd || + !allowDelegatedStaking; const errorMessages = { - gatewayOwner: validators.address(gatewayOwnerWallet), stakeAmount: validators.stakeAmount(amountToStake), - withdrawAmount: validators.withdrawAmount(amountToWithdraw), cannotStake: (balances?.io || 0) < minRequiredStakeToAdd ? `Insufficient balance, at least ${minRequiredStakeToAdd} IO required.` @@ -171,44 +113,17 @@ const StakingModal = ({ return (
    -
    - - +
    +
    + Stake +
    - {ownerWallet ? ( - - ) : ( - <> -
    Gateway Owner:
    - { - setUserEnteredWalletAddress(e.target.value); - }} - maxLength={43} - /> - - )} + Amount:
    - {tab == 0 - ? balances && - `Available: ${remainingBalance >= 0 ? formatWithCommas(+remainingBalance) : '-'} ${ticker}` - : `Available to Withdraw: ${remainingWithdrawalBalance >= 0 ? formatWithCommas(remainingWithdrawalBalance) : '-'} ${ticker}`} + {balances && + `Available: ${remainingBalance >= 0 + ? formatWithCommas(+remainingBalance) + : '-'} ${ticker}`}
    @@ -240,8 +155,8 @@ const StakingModal = ({ disabled={disableInput} readOnly={disableInput} type="text" - placeholder={`Enter amount of ${ticker} to ${tab == 0 ? 'stake' : 'withdraw'}`} - value={tab == 0 ? amountToStake : amountToWithdraw} + placeholder={`Enter amount of ${ticker} to stake`} + value={amountToStake} onChange={(e) => { const textValue = e.target.value; @@ -249,15 +164,10 @@ const StakingModal = ({ return; } - if (tab == 0) { - setAmountToStake(textValue); - } else { - setAmountToWithdraw(textValue); - } + setAmountToStake(textValue); }} - > - {tab == 0 && - gateway && + /> + {gateway && (amountToStake?.length > 0 || (balances?.io || 0) < minRequiredStakeToAdd || !allowDelegatedStaking) && @@ -269,14 +179,6 @@ const StakingModal = ({ tooltipPadding={'3'} /> )} - {tab == 1 && - amountToWithdraw?.length > 0 && - errorMessages.withdrawAmount && ( - - )}
    -
    - {tab == 1 && ( - setWithdrawalType(v)} - > - -
    -
    - - - -
    -

    - 30 day withdrawal period with no fees. -

    -
    -
    - - -
    -
    - - - -
    -

    - Instant withdrawal with 50% fee. -

    -
    -
    -
    - )} -
    - {tab == 1 && withdrawalType == 'expedited' && ( - <> - - - - )} -
    - {tab == 0 && ( - - )} + - {tab == 0 && ( - -

    {EAY_TOOLTIP_TEXT}

    - - {EAY_TOOLTIP_FORMULA} - -
    - } - > - - - } - /> - )} + +

    {EAY_TOOLTIP_TEXT}

    + {EAY_TOOLTIP_FORMULA} +
    + } + > + + + } + />
    { - tab == 0 - ? setShowReviewStakeModal(true) - : setShowReviewWithdrawalModal(true); + setShowReviewStakeModal(true); }} buttonType={ButtonType.PRIMARY} title="Review" @@ -416,19 +248,6 @@ const StakingModal = ({ walletAddress={walletAddress} /> )} - {showReviewWithdrawalModal && gateway && walletAddress && ( - setShowReviewWithdrawalModal(false)} - onSuccess={() => onClose()} - ticker={ticker} - walletAddress={walletAddress} - withdrawalFee={withdrawalFee} - returningAmount={returningAmount} - /> - )}
    ); diff --git a/src/components/modals/StartGatewayModal.tsx b/src/components/modals/StartGatewayModal.tsx index 179d2d0d..503d13c6 100644 --- a/src/components/modals/StartGatewayModal.tsx +++ b/src/components/modals/StartGatewayModal.tsx @@ -1,5 +1,5 @@ -import { IOToken } from '@ar.io/sdk/web'; -import { WRITE_OPTIONS, log } from '@src/constants'; +import { ARIOToken } from '@ar.io/sdk/web'; +import { GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO, WRITE_OPTIONS, log } from '@src/constants'; import { useGlobalState } from '@src/store'; import { showErrorToast } from '@src/utils/toast'; import { useQueryClient } from '@tanstack/react-query'; @@ -9,7 +9,7 @@ import FormRow, { RowType } from '../forms/FormRow'; import { FormRowDef, isFormValid } from '../forms/formData'; import { validateDomainName, - validateIOAmount, + validateARIOAmount, validateNumberRange, validateString, validateTransactionId, @@ -91,8 +91,8 @@ const StartGatewayModal = ({ onClose }: { onClose: () => void }) => { { formPropertyName: 'stake', label: `*Stake (${ticker}):`, - placeholder: `Minimum 10000 ${ticker}`, - validateProperty: validateIOAmount('Stake', ticker, 10000), + placeholder: `Minimum ${GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO} ${ticker}`, + validateProperty: validateARIOAmount('Stake', ticker, GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO), }, { formPropertyName: 'allowDelegatedStaking', @@ -105,7 +105,7 @@ const StartGatewayModal = ({ onClose }: { onClose: () => void }) => { placeholder: allowDelegatedStaking ? `Minimum 10 ${ticker}` : 'Enable Delegated Staking to set this value.', - validateProperty: validateIOAmount('Minimum Delegated Stake', ticker, 10), + validateProperty: validateARIOAmount('Minimum Delegated Stake', ticker, 10), }, { formPropertyName: 'delegatedStakingShareRatio', @@ -149,15 +149,15 @@ const StartGatewayModal = ({ onClose }: { onClose: () => void }) => { delegateRewardShareRatio: allowDelegatedStaking ? parseFloat(String(formState.delegatedStakingShareRatio)) : DEFAULT_DELEGATED_STAKING_REWARD_SHARE_RATIO, - minDelegatedStake: new IOToken( + minDelegatedStake: new ARIOToken( allowDelegatedStaking ? parseFloat(String(formState.minDelegatedStake)) : DEFAULT_DELEGATED_STAKING, - ).toMIO().valueOf(), + ).toMARIO().valueOf(), autoStake: true, - operatorStake: new IOToken( + operatorStake: new ARIOToken( parseFloat(String(formState.stake)), - ).toMIO().valueOf(), + ).toMARIO().valueOf(), }; // UNCOMMENT AND COMMENT OUT JOIN NETWORK FOR DEV WORK diff --git a/src/components/modals/WithdrawAllModal.tsx b/src/components/modals/WithdrawAllModal.tsx index 2933d112..a7a2eaf2 100644 --- a/src/components/modals/WithdrawAllModal.tsx +++ b/src/components/modals/WithdrawAllModal.tsx @@ -1,4 +1,4 @@ -import { AoGateway, mIOToken } from '@ar.io/sdk/web'; +import { AoGateway, mARIOToken } from '@ar.io/sdk/web'; import { WRITE_OPTIONS, log } from '@src/constants'; import { useGlobalState } from '@src/store'; import { showErrorToast } from '@src/utils/toast'; @@ -108,7 +108,7 @@ const WithdrawAllModal = ({ - {new mIOToken(stake.delegatedStake).toIO().valueOf()}{' '} + {new mARIOToken(stake.delegatedStake).toARIO().valueOf()}{' '} {ticker} @@ -122,7 +122,7 @@ const WithdrawAllModal = ({
    Total Withdrawal:
    - {new mIOToken(totalWithdrawalMIO).toIO().valueOf()} {ticker} + {new mARIOToken(totalWithdrawalMIO).toARIO().valueOf()} {ticker}
    diff --git a/src/constants.ts b/src/constants.ts index 846fa777..0ddd3876 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -import { ioDevnetProcessId } from '@ar.io/sdk/web'; +import { arioDevnetProcessId } from '@ar.io/sdk/web'; import * as loglevel from 'loglevel'; import Ar from 'arweave/web/ar'; @@ -17,13 +17,12 @@ export const WRITE_OPTIONS = { }; export const ARIO_DOCS_URL = 'https://docs.ar.io'; -export const IO_PROCESS_ID = new ArweaveTransactionID( - process.env.VITE_IO_PROCESS_ID ?? ioDevnetProcessId, +export const ARIO_PROCESS_ID = new ArweaveTransactionID( + process.env.VITE_ARIO_PROCESS_ID ?? arioDevnetProcessId, ); -export const AO_CU_URL = - process.env.VITE_AO_CU_URL || 'https://cu.ao-testnet.xyz'; +export const AO_CU_URL = process.env.VITE_AO_CU_URL || 'https://cu.ardrive.io'; -export const IO_PROCESS_INFO_URL = `https://www.ao.link/#/entity/${IO_PROCESS_ID.toString()}`; +export const IO_PROCESS_INFO_URL = `https://www.ao.link/#/entity/${ARIO_PROCESS_ID.toString()}`; export const DEFAULT_ARWEAVE_PROTOCOL = process.env.VITE_GATEWAY_PROTOCOL ?? 'https'; @@ -62,3 +61,8 @@ export const OPERATOR_EAY_TOOLTIP_FORMULA = export const NAME_PASS_THRESHOLD = 0.8; export const REFERENCE_GATEWAY_FQDN = process.env.VITE_REFERENCE_GATEWAY_FQDN ?? 'arweave.net'; + +export const GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO = 10000; + +export const REDELEGATION_FEE_TOOLTIP_TEXT = + 'Redelegation fees are assessed at 10% per redelegation performed since the last fee reset, up to 60%. Fees are reset when no redelegations are performed in the last 7 days.'; diff --git a/src/hooks/useBalances.ts b/src/hooks/useBalances.ts index 5ad8fb13..4be49813 100644 --- a/src/hooks/useBalances.ts +++ b/src/hooks/useBalances.ts @@ -1,4 +1,4 @@ -import { mIOToken } from '@ar.io/sdk/web'; +import { mARIOToken } from '@ar.io/sdk/web'; import { AR } from '@src/constants'; import { useGlobalState } from '@src/store'; import { ArweaveTransactionID } from '@src/utils/ArweaveTransactionId'; @@ -26,7 +26,7 @@ const useBalances = (walletAddress?: ArweaveTransactionID) => { ]); const arBalance = +AR.winstonToAr(winstonBalance); - const ioBalance = new mIOToken(mioBalance).toIO().valueOf(); + const ioBalance = new mARIOToken(mioBalance).toARIO().valueOf(); return { ar: arBalance, io: ioBalance }; }, diff --git a/src/hooks/useGatewayInfo.ts b/src/hooks/useGatewayInfo.ts index b9ff6047..7f9b3b4b 100644 --- a/src/hooks/useGatewayInfo.ts +++ b/src/hooks/useGatewayInfo.ts @@ -1,4 +1,4 @@ -import { mIOToken } from '@ar.io/sdk/web'; +import { mARIOToken } from '@ar.io/sdk/web'; import { useGlobalState } from '@src/store'; import { formatDateTime, formatWalletAddress, formatWithCommas } from '@src/utils'; import useGateway from './useGateway'; @@ -29,7 +29,7 @@ export const useGatewayInfo = () => { ['Joined at', formatDateTime(new Date(gateway.startTimestamp))], [ `Stake (${ticker})`, - formatWithCommas(new mIOToken(gateway.operatorStake).toIO().valueOf()), + formatWithCommas(new mARIOToken(gateway.operatorStake).toARIO().valueOf()), ], ['Status', gateway.status], ['Reward Ratio', gateway.settings.delegateRewardShareRatio], diff --git a/src/hooks/useGateways.ts b/src/hooks/useGateways.ts index feff484f..d8bf79fc 100644 --- a/src/hooks/useGateways.ts +++ b/src/hooks/useGateways.ts @@ -1,4 +1,4 @@ -import { AoGateway, AoIORead } from '@ar.io/sdk/web'; +import { AoGateway, AoARIORead } from '@ar.io/sdk/web'; import { useGlobalState } from '@src/store'; import { useQuery } from '@tanstack/react-query'; @@ -6,7 +6,7 @@ const useGateways = () => { const arIOReadSDK = useGlobalState((state) => state.arIOReadSDK); const fetchAllGateways = async ( - arIOReadSDK: AoIORead, + arIOReadSDK: AoARIORead, ): Promise> => { let cursor: string | undefined; const gateways: Record = {}; diff --git a/src/hooks/useOperatorRewards.ts b/src/hooks/useOperatorRewards.ts index 3176c56b..cb202ac2 100644 --- a/src/hooks/useOperatorRewards.ts +++ b/src/hooks/useOperatorRewards.ts @@ -1,4 +1,4 @@ -import { AoGateway, mIOToken } from '@ar.io/sdk/web'; +import { AoGateway, mARIOToken } from '@ar.io/sdk/web'; import { OperatorRewards, calculateOperatorRewards } from '@src/utils/rewards'; import { useEffect, useState } from 'react'; import useGateways from './useGateways'; @@ -16,10 +16,10 @@ const useOperatorRewards = (gateway: AoGateway | undefined) => { (g) => g.status == 'joined', ).length; const operatorRewards = calculateOperatorRewards( - new mIOToken(protocolBalance).toIO(), + new mARIOToken(protocolBalance).toARIO(), numGateways, gateway, - new mIOToken(gateway.operatorStake).toIO(), + new mARIOToken(gateway.operatorStake).toARIO(), ); setOperatorRewards(operatorRewards); } diff --git a/src/hooks/useProtocolBalance.ts b/src/hooks/useProtocolBalance.ts index 1ad50a0b..e28aed33 100644 --- a/src/hooks/useProtocolBalance.ts +++ b/src/hooks/useProtocolBalance.ts @@ -1,4 +1,4 @@ -import { IO_PROCESS_ID } from '@src/constants'; +import { ARIO_PROCESS_ID } from '@src/constants'; import { useGlobalState } from '@src/store'; import { useQuery } from '@tanstack/react-query'; @@ -10,7 +10,7 @@ const useProtocolBalance = () => { queryFn: () => { if (arIOReadSDK) { return arIOReadSDK.getBalance({ - address: IO_PROCESS_ID.toString(), + address: ARIO_PROCESS_ID.toString(), }); } throw new Error('Error: ArIO Read SDK is not initialized'); diff --git a/src/hooks/useRedelegationFee.ts b/src/hooks/useRedelegationFee.ts new file mode 100644 index 00000000..2bcf075d --- /dev/null +++ b/src/hooks/useRedelegationFee.ts @@ -0,0 +1,19 @@ +import { useGlobalState } from '@src/store'; +import { useQuery } from '@tanstack/react-query'; + +const useRedelegationFee = (walletAddress?: string) => { + const arioReadSDK = useGlobalState((state) => state.arIOReadSDK); + + const res = useQuery({ + queryKey: ['redelegationFee', walletAddress], + queryFn: async () => { + if (!arioReadSDK) throw new Error('arIOReadSDK not initialized'); + if (!walletAddress) throw new Error('walletAddress not initialized'); + + return await arioReadSDK.getRedelegationFee({ address: walletAddress }); + }, + }); + return res; +}; + +export default useRedelegationFee; diff --git a/src/hooks/useRewardsEarned.ts b/src/hooks/useRewardsEarned.ts index addf2da4..9ced1fc6 100644 --- a/src/hooks/useRewardsEarned.ts +++ b/src/hooks/useRewardsEarned.ts @@ -1,4 +1,4 @@ -import { mIOToken } from '@ar.io/sdk/web'; +import { mARIOToken } from '@ar.io/sdk/web'; import { useEffect, useState } from 'react'; import useEpochs from './useEpochs'; @@ -27,9 +27,9 @@ const useRewardsEarned = (walletAddress?: string) => { }, 0); setRewardsEarned({ - previousEpoch: new mIOToken(previousEpochRewards).toIO().valueOf(), - totalForPastAvailableEpochs: new mIOToken(totalForPastAvailableEpochs) - .toIO() + previousEpoch: new mARIOToken(previousEpochRewards).toARIO().valueOf(), + totalForPastAvailableEpochs: new mARIOToken(totalForPastAvailableEpochs) + .toARIO() .valueOf(), }); } diff --git a/src/hooks/useRewardsInfo.ts b/src/hooks/useRewardsInfo.ts index 3bdefdb0..21d03db8 100644 --- a/src/hooks/useRewardsInfo.ts +++ b/src/hooks/useRewardsInfo.ts @@ -1,4 +1,4 @@ -import { AoGateway, IOToken, mIOToken } from '@ar.io/sdk/web'; +import { AoGateway, ARIOToken, mARIOToken } from '@ar.io/sdk/web'; import { UserRewards, calculateGatewayRewards, @@ -24,14 +24,14 @@ const useRewardsInfo = (gateway: AoGateway | undefined, userStake: number) => { ? Object.values(gateways).filter((g) => g.status == 'joined').length : 0; const gatewayRewards = calculateGatewayRewards( - new mIOToken(protocolBalance).toIO(), + new mARIOToken(protocolBalance).toARIO(), numGateways, gateway, ); const userRewards = calculateUserRewards( gatewayRewards, - new IOToken(Math.abs(userStake)), + new ARIOToken(Math.abs(userStake)), userStake < 0, ); res = userRewards; diff --git a/src/pages/Dashboard/IOTokenDistributionPanel.tsx b/src/pages/Dashboard/IOTokenDistributionPanel.tsx index 8f7756c1..a1af4b99 100644 --- a/src/pages/Dashboard/IOTokenDistributionPanel.tsx +++ b/src/pages/Dashboard/IOTokenDistributionPanel.tsx @@ -1,4 +1,4 @@ -import { AoTokenSupplyData, mIOToken } from '@ar.io/sdk/web'; +import { AoTokenSupplyData, mARIOToken } from '@ar.io/sdk/web'; import Placeholder from '@src/components/Placeholder'; import useTokenSupply from '@src/hooks/useTokenSupply'; import { useGlobalState } from '@src/store'; @@ -23,25 +23,25 @@ const calculateIODistribution = ( return [ { name: 'Protocol Balance', - value: new mIOToken(tokenSupply.protocolBalance).toIO().valueOf(), + value: new mARIOToken(tokenSupply.protocolBalance).toARIO().valueOf(), }, { name: 'Actively Staked', - value: new mIOToken(tokenSupply.staked + tokenSupply.delegated) - .toIO() + value: new mARIOToken(tokenSupply.staked + tokenSupply.delegated) + .toARIO() .valueOf(), }, { name: 'Pending Withdrawal', - value: new mIOToken(tokenSupply.withdrawn).toIO().valueOf(), + value: new mARIOToken(tokenSupply.withdrawn).toARIO().valueOf(), }, { name: 'In Circulation', - value: new mIOToken(tokenSupply.circulating).toIO().valueOf(), + value: new mARIOToken(tokenSupply.circulating).toARIO().valueOf(), }, { name: 'Locked Supply', - value: new mIOToken(tokenSupply.locked).toIO().valueOf(), + value: new mARIOToken(tokenSupply.locked).toARIO().valueOf(), }, ]; }; @@ -72,7 +72,7 @@ const IOTokenDistributionPanel = () => { data && activeIndex !== undefined ? data[activeIndex].value : tokenSupply?.total - ? new mIOToken(tokenSupply.total).toIO().valueOf() + ? new mARIOToken(tokenSupply.total).toARIO().valueOf() : TOTAL_IO, ), ); diff --git a/src/pages/Dashboard/RewardsDistributionPanel.tsx b/src/pages/Dashboard/RewardsDistributionPanel.tsx index 37675edd..d6c03e44 100644 --- a/src/pages/Dashboard/RewardsDistributionPanel.tsx +++ b/src/pages/Dashboard/RewardsDistributionPanel.tsx @@ -1,4 +1,4 @@ -import { mIOToken } from '@ar.io/sdk/web'; +import { mARIOToken } from '@ar.io/sdk/web'; import Placeholder from '@src/components/Placeholder'; import useEpochs from '@src/hooks/useEpochs'; import { useGlobalState } from '@src/store'; @@ -157,15 +157,15 @@ const RewardsDistributionPanel = () => { .filter((epoch) => epoch !== undefined) .sort((a, b) => a!.epochIndex - b!.epochIndex) .map((epoch) => { - const eligible = new mIOToken( + const eligible = new mARIOToken( epoch!.distributions.totalEligibleRewards, ) - .toIO() + .toARIO() .valueOf(); - const claimed = new mIOToken( + const claimed = new mARIOToken( epoch!.distributions.totalDistributedRewards ?? 0, ) - .toIO() + .toARIO() .valueOf(); return { epoch: epoch!.epochIndex, diff --git a/src/pages/Gateway/ActiveDelegates.tsx b/src/pages/Gateway/ActiveDelegates.tsx index e480e753..853c4041 100644 --- a/src/pages/Gateway/ActiveDelegates.tsx +++ b/src/pages/Gateway/ActiveDelegates.tsx @@ -1,4 +1,4 @@ -import { AoGatewayWithAddress, mIOToken } from '@ar.io/sdk/web'; +import { AoGatewayWithAddress, mARIOToken } from '@ar.io/sdk/web'; import AddressCell from '@src/components/AddressCell'; import Placeholder from '@src/components/Placeholder'; import TableView from '@src/components/TableView'; @@ -30,7 +30,7 @@ const ActiveDelegates = ({ gateway }: { gateway?: AoGatewayWithAddress }) => { if (gateway && gatewayDelegateStakes) { const totalDelegatedStake = gateway.totalDelegatedStake; const data = gatewayDelegateStakes.map((stake) => { - const totalStake = new mIOToken(stake.delegatedStake).toIO().valueOf(); + const totalStake = new mARIOToken(stake.delegatedStake).toARIO().valueOf(); const percentageOfTotalStake = totalDelegatedStake > 0 ? stake.delegatedStake / totalDelegatedStake @@ -84,7 +84,7 @@ const ActiveDelegates = ({ gateway }: { gateway?: AoGatewayWithAddress }) => { {gateway ? (
    {formatWithCommas( - new mIOToken(gateway.totalDelegatedStake).toIO().valueOf(), + new mARIOToken(gateway.totalDelegatedStake).toARIO().valueOf(), )}{' '} {ticker}
    diff --git a/src/pages/Gateway/OperatorStake.tsx b/src/pages/Gateway/OperatorStake.tsx index f268cf2c..e3ec7a34 100644 --- a/src/pages/Gateway/OperatorStake.tsx +++ b/src/pages/Gateway/OperatorStake.tsx @@ -1,9 +1,12 @@ -import { AoGatewayWithAddress, mIOToken } from '@ar.io/sdk/web'; -import Button, { ButtonType } from '@src/components/Button'; +import { AoGatewayWithAddress, ARIOToken, mARIOToken } from '@ar.io/sdk/web'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ThreeDotsIcon } from '@src/components/icons'; import OperatorStakingModal from '@src/components/modals/OperatorStakingModal'; +import OperatorWithdrawalModal from '@src/components/modals/OperatorWithdrawalModal'; +import RedelegateModal, { RedelegateModalProps } from '@src/components/modals/RedelegateModal'; import Placeholder from '@src/components/Placeholder'; import Tooltip from '@src/components/Tooltip'; -import { EAY_TOOLTIP_TEXT, OPERATOR_EAY_TOOLTIP_FORMULA } from '@src/constants'; +import { EAY_TOOLTIP_TEXT, GATEWAY_OPERATOR_STAKE_MINIMUM_ARIO, OPERATOR_EAY_TOOLTIP_FORMULA } from '@src/constants'; import useGateways from '@src/hooks/useGateways'; import useProtocolBalance from '@src/hooks/useProtocolBalance'; import { useGlobalState } from '@src/store'; @@ -23,15 +26,20 @@ const OperatorStake = ({ gateway, walletAddress }: OperatorStakeProps) => { const { data: protocolBalance } = useProtocolBalance(); const { data: gateways } = useGateways(); const [isStakingModalOpen, setIsStakingModalOpen] = useState(false); + const [isWithdrawalModalOpen, setIsWithdrawalModalOpen] = + useState(false); const [eay, setEAY] = useState(); + const [showRedelegateModal, setShowRedelegateModal] = + useState(); + useEffect(() => { if (gateways && gateway && protocolBalance) { const rewards = calculateOperatorRewards( - new mIOToken(protocolBalance).toIO(), + new mARIOToken(protocolBalance).toARIO(), Object.values(gateways).filter((g) => g.status == 'joined').length, gateway, - new mIOToken(gateway.operatorStake).toIO(), + new mARIOToken(gateway.operatorStake).toARIO(), ); setEAY(rewards.EAY); } @@ -46,14 +54,59 @@ const OperatorStake = ({ gateway, walletAddress }: OperatorStakeProps) => { {gateway?.gatewayAddress === walletAddress && gateway?.status != 'leaving' && ( -