diff --git a/.changeset/pretty-wasps-build.md b/.changeset/pretty-wasps-build.md new file mode 100644 index 000000000..f53d86883 --- /dev/null +++ b/.changeset/pretty-wasps-build.md @@ -0,0 +1,62 @@ +--- +'@status-im/components': patch +'hub': major +--- + +### Staking & Vault Management System + +- **New Hooks:** + + - Staking functionality with multiplier points tracking + - Vault locking mechanisms + - Token approval and withdrawal flows + - Account vaults management + - Faucet integration for testing + - Emergency mode functionality + +- **User Experience:** + - Lock status indicators in vault selection UI + - Improved table overflow handling + - Enhanced slider configuration for vault lockup periods + +### UI Components + +- **New Modals:** + + - Withdraw modal for vault withdrawals + - Lock configuration modal for vault locking + - Compound modal for compounding rewards + - Promo modal for staking page + - Action status component (formerly progress dialog) + +- **UI Improvements:** + - New icons added + - Enhanced vault table components + - Better modal state management + +### Authentication & Web3 Integration + +- **SIWE Integration:** + - Sign-In With Ethereum (SIWE) for wallet authentication + - ConnectKit integration for wallet connection + - Wagmi configuration and providers setup + +### State Management & Data Fetching + +- **Dependencies Added:** + - `@tanstack/react-query` for server state management + - `react-table` for table data management + - Improved overall state management patterns + +### Emergency Features + +- Emergency mode functionality for vault operations +- Enhanced safety controls for vault lockup periods + +## Refactoring & Maintenance + +### Code Quality Improvements + +- Simplified vault table action handlers +- Removed unnecessary onClick logic for modal state management +- Renamed progress dialog to action status component for clarity diff --git a/.npmrc b/.npmrc index 798eb5648..f54ec269c 100644 --- a/.npmrc +++ b/.npmrc @@ -17,4 +17,4 @@ engine-strict=true ignore-workspace-root-check=true ; https://pnpm.io/npmrc#node-options ; why: https://github.com/vercel/next.js/discussions/70423 -node-options=--network-family-autoselection-attempt-timeout=500 \ No newline at end of file +# node-options=--network-family-autoselection-attempt-timeout=500 \ No newline at end of file diff --git a/apps/hub/package.json b/apps/hub/package.json index bffe62278..c7d6d6160 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -14,10 +14,13 @@ "clean": "rimraf .next .vercel/output node_modules" }, "dependencies": { + "@hookform/devtools": "^4.3.1", + "@hookform/resolvers": "^3.1.1", "@status-im/colors": "workspace:*", - "@status-im/icons": "workspace:*", "@status-im/components": "workspace:*", + "@status-im/icons": "workspace:*", "@status-im/status-network": "workspace:*", + "@tanstack/react-table": "^8.21.3", "@vercel/analytics": "^1.5.0", "cva": "1.0.0-beta.1", "framer-motion": "^12.0.6", @@ -25,14 +28,17 @@ "next-mdx-remote": "^5.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.45.1", "rehype-slug": "^6.0.0", + "siwe": "^2.3.2", "ts-pattern": "^5.6.2", - "zod": "^3.24.1" + "viem": "^2.21.1", + "zod": "^3.21.4" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@next/eslint-plugin-next": "15.3.0", "@status-im/eslint-config": "workspace:*", - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/apps/hub/public/modal/bg-gradient.png b/apps/hub/public/modal/bg-gradient.png new file mode 100644 index 000000000..f00944338 Binary files /dev/null and b/apps/hub/public/modal/bg-gradient.png differ diff --git a/apps/hub/public/modal/connector-1.png b/apps/hub/public/modal/connector-1.png new file mode 100644 index 000000000..c3b181104 Binary files /dev/null and b/apps/hub/public/modal/connector-1.png differ diff --git a/apps/hub/public/modal/connector-2.png b/apps/hub/public/modal/connector-2.png new file mode 100644 index 000000000..5e5f515f4 Binary files /dev/null and b/apps/hub/public/modal/connector-2.png differ diff --git a/apps/hub/public/modal/dragon.png b/apps/hub/public/modal/dragon.png new file mode 100644 index 000000000..d168ed178 Binary files /dev/null and b/apps/hub/public/modal/dragon.png differ diff --git a/apps/hub/public/piggy-bank.png b/apps/hub/public/piggy-bank.png new file mode 100644 index 000000000..68bd09bf5 Binary files /dev/null and b/apps/hub/public/piggy-bank.png differ diff --git a/apps/hub/src/app/_components/icons/close-icon.tsx b/apps/hub/src/app/_components/icons/close-icon.tsx new file mode 100644 index 000000000..bfc2c617a --- /dev/null +++ b/apps/hub/src/app/_components/icons/close-icon.tsx @@ -0,0 +1,20 @@ +import type { SVGProps } from 'react' + +const CloseIcon = (props: SVGProps) => ( + + + +) +export default CloseIcon diff --git a/apps/hub/src/app/_components/icons/index.ts b/apps/hub/src/app/_components/icons/index.ts index eba9c7c31..fbde49ee3 100644 --- a/apps/hub/src/app/_components/icons/index.ts +++ b/apps/hub/src/app/_components/icons/index.ts @@ -1,5 +1,6 @@ // Custom Figma icons exported directly from design export { default as BridgeIcon } from './bridge-icon' +export { default as CloseIcon } from './close-icon' export { default as DepositIcon } from './deposit-icon' export { default as DiscoverIcon } from './discover-icon' export { default as DocsIcon } from './docs-icon' @@ -12,8 +13,12 @@ export { default as LaunchIcon } from './launch-icon' export { default as MintIcon } from './mint-icon' export { default as PercentIcon } from './percent-icon' export { default as PlusIcon } from './plus-icon' +export { default as ProcessingIcon } from './processing-icon' +export { default as RejectIcon } from './reject-icon' export { default as SettingsIcon } from './settings-icon' +export { default as SNTIcon } from './snt-icon' export { default as StakeIcon } from './stake-icon' export { default as SubmitAppIcon } from './submit-app-icon' export { default as SwapIcon } from './swap-icon' export { default as TwitterIcon } from './twitter-icon' +export { default as VaultIcon } from './vault-icon' diff --git a/apps/hub/src/app/_components/icons/launch-icon.tsx b/apps/hub/src/app/_components/icons/launch-icon.tsx index 676cf2863..18e105759 100644 --- a/apps/hub/src/app/_components/icons/launch-icon.tsx +++ b/apps/hub/src/app/_components/icons/launch-icon.tsx @@ -1,52 +1,27 @@ -export default function LaunchIcon({ - className = '', - ...props -}: React.SVGProps) { - return ( - - - - - - - - - - - - - - ) -} +import type { SVGProps } from 'react' + +const LaunchIcon = (props: SVGProps) => ( + + + + +) +export default LaunchIcon diff --git a/apps/hub/src/app/_components/icons/processing-icon.tsx b/apps/hub/src/app/_components/icons/processing-icon.tsx new file mode 100644 index 000000000..279c229ff --- /dev/null +++ b/apps/hub/src/app/_components/icons/processing-icon.tsx @@ -0,0 +1,34 @@ +import type { SVGProps } from 'react' + +const ProcessingIcon = ({ ...props }: SVGProps) => ( + + + + + +) +export default ProcessingIcon diff --git a/apps/hub/src/app/_components/icons/reject-icon.tsx b/apps/hub/src/app/_components/icons/reject-icon.tsx new file mode 100644 index 000000000..9f2d22c41 --- /dev/null +++ b/apps/hub/src/app/_components/icons/reject-icon.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +const RejectIcon = (props: SVGProps) => ( + + + + + + + + + + +) +export default RejectIcon diff --git a/apps/hub/src/app/_components/icons/snt-icon.tsx b/apps/hub/src/app/_components/icons/snt-icon.tsx new file mode 100644 index 000000000..acc29acbb --- /dev/null +++ b/apps/hub/src/app/_components/icons/snt-icon.tsx @@ -0,0 +1,22 @@ +import Image from 'next/image' + +export default function SNTIcon() { + return ( +
+ SNT + Karma +
+ ) +} diff --git a/apps/hub/src/app/_components/icons/vault-icon.tsx b/apps/hub/src/app/_components/icons/vault-icon.tsx new file mode 100644 index 000000000..8f3eebf8e --- /dev/null +++ b/apps/hub/src/app/_components/icons/vault-icon.tsx @@ -0,0 +1,32 @@ +import type { SVGProps } from 'react' + +export default function VaultIcon({ + className, + ...props +}: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/hub/src/app/_components/stake/action-status-dialog.tsx b/apps/hub/src/app/_components/stake/action-status-dialog.tsx new file mode 100644 index 000000000..f20f46969 --- /dev/null +++ b/apps/hub/src/app/_components/stake/action-status-dialog.tsx @@ -0,0 +1,91 @@ +import * as Dialog from '@radix-ui/react-dialog' +import { CloseIcon } from '@status-im/icons/20' +import { match } from 'ts-pattern' + +import { ProcessingIcon, RejectIcon, VaultIcon } from '../icons' +import { type ActionStatusState } from './types/action-status' + +type Props = { + open: boolean + onClose: () => void + title?: string + description?: string + children?: React.ReactNode + state?: ActionStatusState + showCloseButton?: boolean + content?: React.ReactNode +} + +const ActionStatusDialog = (props: Props) => { + const { + open, + onClose, + title, + description, + children, + state = 'pending', + showCloseButton = true, + content, + } = props + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen && showCloseButton) { + onClose() + } + } + + const mapIconToState = (state: ActionStatusState) => { + return match(state) + .with('pending', () => ) + .with('processing', () => ) + .with('error', () => ) + .with('success', () => ) + .otherwise(() => null) + } + + return ( + + {children} + + + + +
+
+ {showCloseButton && ( + + + + )} + {content ? ( +
{content}
+ ) : ( +
+ {mapIconToState(state)} + +

+ {title} +

+
+ {description && ( + + + {description} + + + )} +
+ )} +
+ + + + ) +} + +export { ActionStatusDialog } diff --git a/apps/hub/src/app/_components/stake/compound-status-content.tsx b/apps/hub/src/app/_components/stake/compound-status-content.tsx new file mode 100644 index 000000000..9f6a3c093 --- /dev/null +++ b/apps/hub/src/app/_components/stake/compound-status-content.tsx @@ -0,0 +1,93 @@ +import * as Dialog from '@radix-ui/react-dialog' +import { InfoIcon } from '@status-im/icons/16' +import { formatUnits } from 'viem' + +import { LaunchIcon } from '~components/icons' +import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useWeightedBoost } from '~hooks/useWeightedBoost' +import { formatSNT } from '~utils/currency' + +import { SNT_TOKEN } from '../../_constants' + +export const CompoundStatusContent = () => { + const { data: vaults } = useStakingVaults() + const weightedBoost = useWeightedBoost(vaults) + const { data: multiplierPointsData } = useMultiplierPointsBalance() + + const earnRateWithBoost = Math.floor(weightedBoost.totalStaked * 0.05) + const earnRateWithoutBoost = Math.floor(weightedBoost.totalStaked * 0.05) + + return ( + <> +
+ +
+ +
+
+ +

+ {`Ready to compound ${formatSNT( + formatUnits( + multiplierPointsData?.totalUncompounded ?? 0n, + SNT_TOKEN.decimals + ) + )} points`} +

+
+ +

+ Please sign the message in your wallet. +

+
+
+ +
+
+

+ Total compounded +

+

+ {`${formatSNT( + formatUnits( + multiplierPointsData?.totalMpRedeemed ?? 0n, + SNT_TOKEN.decimals + ) + )} points`} +

+
+ +
+

+ Your earn rate at {weightedBoost.formatted} boost +

+

+ {earnRateWithBoost} Karma / day +

+
+ +
+

+ Equivalent at x0.00 boost +

+

+ {earnRateWithoutBoost} Karma / day +

+
+
+ +
+
+ +
+

+ Boost the rate at which you receive Karma. More points you compound, + the higher your rate. The longer you lock your vault, the higher + your boost, and the faster you accumulate Karma. +

+
+
+ + ) +} diff --git a/apps/hub/src/app/_components/stake/hooks/use-action-status-content.tsx b/apps/hub/src/app/_components/stake/hooks/use-action-status-content.tsx new file mode 100644 index 000000000..1b236d7d4 --- /dev/null +++ b/apps/hub/src/app/_components/stake/hooks/use-action-status-content.tsx @@ -0,0 +1,170 @@ +import { match } from 'ts-pattern' + +import { type VaultState } from '~hooks/useVaultStateMachine' +import { formatSNT } from '~utils/currency' + +import { CompoundStatusContent } from '../compound-status-content' +import { type ActionStatusContent } from '../types/action-status' + +/** + * Hook to generate action status dialog content based on vault state + * Maps vault state machine states to user-facing dialog content + */ +export function useActionStatusContent( + state: VaultState +): ActionStatusContent | null { + return ( + match(state) + // SIWE flow + .with({ type: 'siwe', step: 'initialize' }, () => ({ + title: 'Sign in', + description: 'Please sign the message in your wallet.', + state: 'pending', + showCloseButton: true, + })) + .with({ type: 'siwe', step: 'processing' }, () => ({ + title: 'Signing in', + description: 'Wait a moment.', + state: 'processing', + showCloseButton: true, + })) + .with({ type: 'siwe', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Create Vault flow + .with({ type: 'createVault', step: 'initialize' }, () => ({ + title: 'Ready to create new vault', + description: 'Please sign the message in your wallet.', + state: 'pending', + showCloseButton: true, + })) + .with({ type: 'createVault', step: 'processing' }, () => ({ + title: 'Creating new vault', + description: 'Wait a moment.', + state: 'processing', + showCloseButton: true, + })) + .with({ type: 'createVault', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Increase Allowance flow + .with({ type: 'increaseAllowance', step: 'initialize' }, () => ({ + title: 'Increase token allowance', + description: 'Please sign the message in your wallet.', + state: 'pending', + showCloseButton: false, + })) + .with({ type: 'increaseAllowance', step: 'processing' }, () => ({ + title: 'Increasing token allowance', + description: 'Wait a moment.', + state: 'processing', + showCloseButton: false, + })) + .with({ type: 'increaseAllowance', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Staking flow + .with( + { + type: 'staking', + step: 'initialize', + }, + state => ({ + title: `Ready to stake ${formatSNT(state.amount ?? 0, { includeSymbol: true })}`, + description: 'Please sign the message in your wallet.', + state: 'pending', + showCloseButton: true, + }) + ) + + .with({ type: 'staking', step: 'processing' }, state => ({ + title: `Staking ${formatSNT(state.amount ?? 0, { includeSymbol: true })}`, + description: 'Wait a moment...', + state: 'processing', + showCloseButton: false, + })) + .with({ type: 'staking', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Withdraw flow (goes directly to processing, no initialize step) + .with({ type: 'withdraw', step: 'processing' }, state => ({ + title: `Withdrawing ${formatSNT(state.amount ?? 0, { includeSymbol: true })}`, + description: 'Wait a moment...', + state: 'processing', + showCloseButton: false, + })) + .with({ type: 'withdraw', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Lock flow + .with({ type: 'lock', step: 'initialize' }, () => ({ + title: 'Ready to lock vault', + description: 'Please sign the message in your wallet.', + state: 'pending', + showCloseButton: true, + })) + .with({ type: 'lock', step: 'processing' }, () => ({ + title: 'Locking vault', + description: 'Wait a moment...', + state: 'processing', + showCloseButton: false, + })) + .with({ type: 'lock', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // compound flow + .with({ type: 'compound', step: 'initialize' }, state => ({ + state: 'pending', + showCloseButton: true, + content: , + })) + .with({ type: 'compound', step: 'processing' }, state => ({ + title: `Compounding ${formatSNT(state.amount ?? 0)} points`, + description: 'Wait a moment...', + state: 'processing', + showCloseButton: false, + })) + .with({ type: 'compound', step: 'rejected' }, () => ({ + title: 'Request was rejected', + description: 'Request was rejected by user', + state: 'error', + showCloseButton: true, + })) + + // Success + .with({ type: 'success' }, () => ({ + title: 'Success!', + description: 'Your transaction completed successfully', + state: 'success', + showCloseButton: true, + })) + + // Idle - no dialog + .with({ type: 'idle' }, () => null) + .exhaustive() + ) +} diff --git a/apps/hub/src/app/_components/stake/promo-modal.tsx b/apps/hub/src/app/_components/stake/promo-modal.tsx new file mode 100644 index 000000000..932f509b0 --- /dev/null +++ b/apps/hub/src/app/_components/stake/promo-modal.tsx @@ -0,0 +1,123 @@ +'use client' + +import * as Dialog from '@radix-ui/react-dialog' +import { Tag } from '@status-im/components' +import { CloseIcon, ExternalIcon } from '@status-im/icons/20' +import { Button, ButtonLink } from '@status-im/status-network/components' +import { ConnectKitButton } from 'connectkit' +import Image from 'next/image' + +type Props = { + open: boolean + onClose: () => void + children: React.ReactNode +} + +const PromoModal = (props: Props) => { + const { open, onClose, children } = props + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) { + onClose() + } + } + + return ( + + {children} + + + + +
+ + + +
+
+
+ +
+
+ + +
+ +

+ Connect to +
+ Status Network +

+
+ + +

+ Use Status L2 features in Chrome with the safety and + control of Status. +

+
+
+ +
+ + Install Status Wallet Connector + + + + + {({ show, isConnected }) => ( + + )} + +
+
+ +
+ Status Wallet Connector + Status Wallet Connector + Dragon +
+
+
+ + + + ) +} + +export { PromoModal } diff --git a/apps/hub/src/app/_components/stake/types/action-status.ts b/apps/hub/src/app/_components/stake/types/action-status.ts new file mode 100644 index 000000000..3605399fa --- /dev/null +++ b/apps/hub/src/app/_components/stake/types/action-status.ts @@ -0,0 +1,19 @@ +/** + * Represents the current state of an action status dialog + */ +export type ActionStatusState = + | 'pending' // Waiting for user action (sign, approve) + | 'processing' // Transaction in progress + | 'error' // Failed/rejected + | 'success' // Completed successfully + +/** + * Content configuration for action status dialog + */ +export interface ActionStatusContent { + state: ActionStatusState + title?: string + description?: string + showCloseButton?: boolean + content?: React.ReactNode +} diff --git a/apps/hub/src/app/_components/vault-select.tsx b/apps/hub/src/app/_components/vault-select.tsx new file mode 100644 index 000000000..51728f087 --- /dev/null +++ b/apps/hub/src/app/_components/vault-select.tsx @@ -0,0 +1,188 @@ +'use client' + +import { useState } from 'react' + +import { DropdownMenu } from '@status-im/components' +import { + AddSmallIcon, + DropdownIcon, + LockedIcon, + UnlockedIcon, +} from '@status-im/icons/20' + +import { formatSNT } from '~utils/currency' +import { isVaultLocked } from '~utils/vault' + +import type { StakingVault } from '~hooks/useStakingVaults' + +// ============================================================================ +// Types +// ============================================================================ + +interface VaultSelectProps { + /** + * List of available vaults to select from + */ + vaults: StakingVault[] + /** + * Currently selected vault address + */ + value: string + /** + * Callback when vault selection changes + */ + onChange: (vaultAddress: string) => void + /** + * Whether the select is disabled + * @default false + */ + disabled?: boolean + /** + * Placeholder text when no vault is selected + * @default "Add new vault" + */ + placeholder?: string +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Gets display label for a vault option + */ +function getVaultLabel(vault: StakingVault, index: number): string { + const stakedAmount = vault.data?.stakedBalance + ? formatSNT(vault.data.stakedBalance) + : '0' + + // Format: #1 - 0xd233...34c4, 100,000,000 SNT + const shortAddress = `${vault.address.slice(0, 6)}...${vault.address.slice(-4)}` + return `#${index + 1} - ${shortAddress}, ${stakedAmount} SNT` +} + +// ============================================================================ +// Component +// ============================================================================ + +/** + * Vault selection dropdown component + * + * Displays a list of user vaults with their staked balances. + * Allows selecting a vault or creating a new one. + * + * @example + * ```tsx + * function StakeForm() { + * const { data: vaults } = useAccountVaults() + * const [selectedVault, setSelectedVault] = useState('') + * + * return ( + * + * ) + * } + * ``` + */ +export function VaultSelect({ + vaults, + value, + onChange, + disabled = false, + placeholder = 'Add new vault', +}: VaultSelectProps) { + const [open, setOpen] = useState(false) + + // Find the selected vault to display its label + const selectedVault = vaults.find(vault => vault.address === value) + const selectedIndex = vaults.findIndex(vault => vault.address === value) + + const isLocked = isVaultLocked(selectedVault?.data?.lockUntil) + + const displayLabel = + selectedVault && selectedIndex !== -1 + ? getVaultLabel(selectedVault, selectedIndex) + : placeholder + + // If no vaults exist, show the "New vault" option (disabled) + const hasVaults = vaults.length > 0 + + const trigger = ( + + ) + + const content = ( + + } + label={placeholder} + selected={!value} + onSelect={() => { + onChange('') + setOpen(false) + }} + /> + {hasVaults ? ( + <> + {vaults.map((vault, index) => { + const isLocked = isVaultLocked(vault.data?.lockUntil) + + return ( + + ) : ( + + ) + } + label={getVaultLabel(vault, index)} + selected={vault.address === value} + onSelect={() => { + onChange(vault.address) + setOpen(false) + }} + /> + ) + })} + + ) : null} + + ) + + return ( + + {trigger} + {content} + + ) +} diff --git a/apps/hub/src/app/_components/vaults/modals/base-vault-modal.tsx b/apps/hub/src/app/_components/vaults/modals/base-vault-modal.tsx new file mode 100644 index 000000000..34e9e653e --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/base-vault-modal.tsx @@ -0,0 +1,72 @@ +'use client' + +import * as Dialog from '@radix-ui/react-dialog' +import { CloseIcon } from '@status-im/icons/20' + +import type { ReactNode } from 'react' + +interface BaseVaultModalProps { + open?: boolean + onOpenChange?: (open: boolean) => void + onClose: () => void + title: string + description: string + children?: ReactNode + trigger?: ReactNode +} + +/** + * Base modal component for vault-related actions. + * Provides consistent dialog wrapper with close button, overlay, and styling. + */ +export function BaseVaultModal(props: BaseVaultModalProps) { + const { open, onOpenChange, onClose, title, description, children, trigger } = + props + + const handleOpenChange = (nextOpen: boolean) => { + if (onOpenChange) { + onOpenChange(nextOpen) + } + if (!nextOpen) { + onClose() + } + } + + return ( + + {trigger && {trigger}} + + + +
+ + + + +
+ +
+ + {title} + +
+
+ +
+ {description} +
+
+
+ + {children} +
+
+
+
+ ) +} diff --git a/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/constants.ts b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/constants.ts new file mode 100644 index 000000000..7e93676e1 --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/constants.ts @@ -0,0 +1,33 @@ +/** + * Time conversion constants for vault locking calculations + */ +export const TIME_CONSTANTS = { + SECONDS_PER_MINUTE: 60, + MINUTES_PER_HOUR: 60, + HOURS_PER_DAY: 24, + DAYS_PER_YEAR: 365, +} as const + +/** + * Calculated time conversion values + */ +export const SECONDS_PER_DAY = + TIME_CONSTANTS.SECONDS_PER_MINUTE * + TIME_CONSTANTS.MINUTES_PER_HOUR * + TIME_CONSTANTS.HOURS_PER_DAY + +export const MILLISECONDS_PER_DAY = SECONDS_PER_DAY * 1000 + +/** + * Date formatting constants + */ +export const DATE_FORMAT = { + PAD_LENGTH: 2, + PAD_CHAR: '0', + SEPARATOR: '/', +} as const + +export const DEFAULT_LOCK_PERIOD = { + INITIAL_YEARS: '0.25', + INITIAL_DAYS: '90', +} as const diff --git a/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/index.tsx b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/index.tsx new file mode 100644 index 000000000..ccd635b4d --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/index.tsx @@ -0,0 +1,120 @@ +'use client' + +import { useReadContract } from 'wagmi' + +import { vaultAbi } from '~constants/contracts' +import { useLockVault } from '~hooks/useLockVault' + +import { BaseVaultModal } from '../base-vault-modal' +import { DEFAULT_LOCK_PERIOD } from './constants' +import { LockVaultForm } from './lock-vault-form' + +import type { HTMLAttributes } from 'react' +import type { Address } from 'viem' + +type ActionButton = HTMLAttributes & { + label: string + disabled?: boolean +} + +interface LockVaultModalProps { + open?: boolean + onOpenChange?: (open: boolean) => void + onClose: () => void + vaultAddress: Address + title: string + description: string + children?: React.ReactNode + initialYears?: string + initialDays?: string + infoMessage?: string + errorMessage?: string | null + onValidate?: (years: string, days: string) => string | null + actions: [ActionButton, ActionButton] +} + +/** + * Modal for locking vault to earn multiplier boost + */ +export function LockVaultModal(props: LockVaultModalProps) { + const { + open, + onOpenChange, + onClose, + children, + title, + vaultAddress, + description, + infoMessage, + errorMessage, + onValidate, + actions, + } = props + + const { mutate: lockVault } = useLockVault(vaultAddress) + + const { data: lockUntil } = useReadContract({ + abi: vaultAbi, + address: vaultAddress, + functionName: 'lockUntil', + }) as { data: bigint } + + // Calculate initial values based on current lockUntil for extensions + // Only consider it "extending" if the vault is currently locked (lockUntil > now) + const now = BigInt(Math.floor(Date.now() / 1000)) + const isExtending = lockUntil && lockUntil > now + const calculatedInitialDays = isExtending + ? Math.ceil((Number(lockUntil) - Math.floor(Date.now() / 1000)) / 86400) + : undefined + const calculatedInitialYears = calculatedInitialDays + ? (calculatedInitialDays / 365).toFixed(2) + : undefined + + // Use calculated values for extensions, props for new locks + const finalInitialDays = isExtending + ? String(calculatedInitialDays) + : DEFAULT_LOCK_PERIOD.INITIAL_DAYS + const finalInitialYears = isExtending + ? calculatedInitialYears + : DEFAULT_LOCK_PERIOD.INITIAL_YEARS + + /** + * Handle the submit of the lock vault form + * + * @param lockPeriodInSeconds - The increased lock duration in seconds + * The smart contract handles all calculations internally via _calculateLock + */ + const handleSubmit = (lockPeriodInSeconds: bigint) => { + // Always pass the duration in seconds + // The smart contract's _calculateLock function handles: + // - Calculating delta MP + // - Calculating new lock end time (max(currentLockEnd, now) + increasedLockSeconds) + // - Validation (min/max periods, MP overflow checks) + lockVault({ + lockPeriodInSeconds, + }) + } + + return ( + + + + ) +} diff --git a/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-duration-slider.tsx b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-duration-slider.tsx new file mode 100644 index 000000000..c1c2ccf34 --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-duration-slider.tsx @@ -0,0 +1,46 @@ +'use client' + +interface SliderConfig { + minLabel: string + maxLabel: string + minDays: number + maxDays: number +} + +interface LockDurationSliderProps { + sliderConfig: SliderConfig + value: number + onChange: (value: number) => void +} + +/** + * Slider component for selecting vault lock duration + */ +export function LockDurationSlider(props: LockDurationSliderProps) { + const { sliderConfig, value, onChange } = props + + const handleSliderChange = (e: React.ChangeEvent) => { + const inputDays = parseFloat(e.target.value) + onChange(inputDays) + } + + return ( +
+
+ +
+
+ {sliderConfig.minLabel} + {sliderConfig.maxLabel} +
+
+ ) +} diff --git a/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-vault-form.tsx b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-vault-form.tsx new file mode 100644 index 000000000..5b555b7ef --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/lock-vault-modal/lock-vault-form.tsx @@ -0,0 +1,357 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { ContextTag, Input } from '@status-im/components' +import { InfoIcon } from '@status-im/icons/16' +import { IncorrectIcon } from '@status-im/icons/20' +import { Button } from '@status-im/status-network/components' +import { useForm } from 'react-hook-form' +import { z } from 'zod' + +import { useSliderConfig } from '~hooks/useSliderConfig' + +import { DATE_FORMAT, DEFAULT_LOCK_PERIOD, SECONDS_PER_DAY } from './constants' +import { LockDurationSlider } from './lock-duration-slider' + +import type { HTMLAttributes } from 'react' + +const DAYS_PER_YEAR = 365 + +type ActionButton = HTMLAttributes & { + label: string + disabled?: boolean +} + +const createFormSchema = () => { + return z.object({ + years: z.string(), + days: z.string(), + }) +} + +type FormValues = z.infer> + +interface LockVaultFormProps { + onSubmit: (lockPeriodInSeconds: bigint) => void + onClose: () => void + actions: [ActionButton, ActionButton] + initialYears?: string + initialDays?: string + infoMessage?: string + errorMessage?: string | null + onValidate?: (years: string, days: string) => string | null + /** Current lockUntil timestamp (for vault extensions) */ + currentLockUntil?: bigint +} + +/** + * Form component for vault lock configuration + */ +export function LockVaultForm(props: LockVaultFormProps) { + const { + initialYears, + initialDays, + infoMessage = 'Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks.', + errorMessage: externalErrorMessage, + onValidate, + onSubmit, + onClose, + actions, + currentLockUntil, + } = props + + const [closeAction, submitAction] = actions + + const { watch, setValue, handleSubmit } = useForm({ + resolver: zodResolver(createFormSchema()), + defaultValues: { + years: initialYears || DEFAULT_LOCK_PERIOD.INITIAL_YEARS, + days: initialDays || DEFAULT_LOCK_PERIOD.INITIAL_DAYS, + }, + }) + + const { data: sliderConfigQuery } = useSliderConfig() + + const sliderConfig = useMemo(() => { + const minSeconds = sliderConfigQuery?.min || 7776000 // fallback: 90 days in seconds + const maxSeconds = sliderConfigQuery?.max || 126144000 // fallback: 4 years in seconds + + const minDays = Math.round(minSeconds / SECONDS_PER_DAY) + const maxDays = Math.round(maxSeconds / SECONDS_PER_DAY) + + const minYears = minDays / DAYS_PER_YEAR + const maxYears = maxDays / DAYS_PER_YEAR + + const minLabel = + minYears < 1 ? `${minDays} days` : `${minYears.toFixed(1)} years` + const maxLabel = + maxYears < 1 ? `${maxDays} days` : `${Math.round(maxYears)} years` + + return { + minLabel, + maxLabel, + minDays, + maxDays, + initialPosition: 50, + } + }, [sliderConfigQuery]) + + const years = watch('years') + const days = watch('days') + + const initialSliderValue = + parseInt(initialYears || DEFAULT_LOCK_PERIOD.INITIAL_YEARS) * + DAYS_PER_YEAR + + parseInt(initialDays || DEFAULT_LOCK_PERIOD.INITIAL_DAYS) + + const [sliderValue, setSliderValue] = useState(initialSliderValue) + + const calculateUnlockDate = (totalDays: number): string => { + const totalSeconds = totalDays * SECONDS_PER_DAY + // Always calculate from today for consistent UX + const unlockTimestamp = Math.floor(Date.now() / 1000) + totalSeconds + + // Convert to Date for display (multiply by 1000 for milliseconds) + const unlockDate = new Date(unlockTimestamp * 1000) + + const day = String(unlockDate.getDate()).padStart( + DATE_FORMAT.PAD_LENGTH, + DATE_FORMAT.PAD_CHAR + ) + const month = String(unlockDate.getMonth() + 1).padStart( + DATE_FORMAT.PAD_LENGTH, + DATE_FORMAT.PAD_CHAR + ) + const year = unlockDate.getFullYear() + + return `${day}${DATE_FORMAT.SEPARATOR}${month}${DATE_FORMAT.SEPARATOR}${year}` + } + + const calculatedUnlockDate = useMemo(() => { + const daysValue = parseInt( + days || initialDays || DEFAULT_LOCK_PERIOD.INITIAL_DAYS + ) + return calculateUnlockDate(daysValue) + }, [days, initialDays]) + + useEffect(() => { + const daysValue = parseInt(days || DEFAULT_LOCK_PERIOD.INITIAL_DAYS) + if (!isNaN(daysValue) && daysValue !== sliderValue) { + setSliderValue(daysValue) + } + }, [days, sliderValue]) + + const handleVaultLockOrExtend = async (data: FormValues) => { + const totalDays = parseInt(data.days || DEFAULT_LOCK_PERIOD.INITIAL_DAYS) + const userDesiredTotalLockSeconds = BigInt(totalDays * SECONDS_PER_DAY) + const isExtending = currentLockUntil && currentLockUntil > 0n + + let increasedLockSeconds: bigint + + if (isExtending) { + // When extending: calculate how much additional time to add + // to reach the user's desired total lock time from now + const now = BigInt(Math.floor(Date.now() / 1000)) + const currentRemainingSeconds = + currentLockUntil > now ? currentLockUntil - now : 0n + + // If user wants total lock of X seconds from now, and vault currently locked for Y seconds from now, + // we need to add (X - Y) seconds + increasedLockSeconds = + userDesiredTotalLockSeconds - currentRemainingSeconds + + // Ensure we're adding at least some time (can't decrease lock) + if (increasedLockSeconds < 0n) { + increasedLockSeconds = 0n + } + } else { + // New lock: just use the duration as-is + increasedLockSeconds = userDesiredTotalLockSeconds + } + + onClose() + onSubmit(increasedLockSeconds) + } + + const handleSliderChange = (inputDays: number) => { + setSliderValue(inputDays) + + const yearsValue = (inputDays / DAYS_PER_YEAR).toFixed(2) + + setValue('years', yearsValue) + setValue('days', Math.round(inputDays).toString()) + } + + const handleYearsChange = (value: string) => { + setValue('years', value) + + const yearsValue = parseFloat(value || '0') + if (!isNaN(yearsValue)) { + const totalDays = Math.round(yearsValue * DAYS_PER_YEAR) + const clampedDays = Math.max( + sliderConfig.minDays, + Math.min(totalDays, sliderConfig.maxDays) + ) + + setSliderValue(clampedDays) + setValue('days', clampedDays.toString()) + } + } + + const handleDaysChange = (value: string) => { + setValue('days', value) + + const inputDays = parseInt(value || DEFAULT_LOCK_PERIOD.INITIAL_DAYS) + if (!isNaN(inputDays)) { + const clampedDays = Math.max( + sliderConfig.minDays, + Math.min(inputDays, sliderConfig.maxDays) + ) + + setSliderValue(clampedDays) + setValue('years', (clampedDays / DAYS_PER_YEAR).toFixed(2)) + } + } + + const getValidationError = (): string | null => { + const yearsValue = parseFloat(years || DEFAULT_LOCK_PERIOD.INITIAL_YEARS) + const daysValue = parseInt(days || DEFAULT_LOCK_PERIOD.INITIAL_DAYS) + + if (daysValue > 0 && daysValue < sliderConfig.minDays) { + return `Minimum lock time is ${sliderConfig.minLabel}` + } + + if (daysValue > sliderConfig.maxDays) { + return `Maximum lock time is ${sliderConfig.maxLabel}` + } + + const minYears = sliderConfig.minDays / DAYS_PER_YEAR + if (yearsValue > 0 && yearsValue < minYears) { + return `Minimum lock time is ${sliderConfig.minLabel}` + } + + const maxYears = sliderConfig.maxDays / DAYS_PER_YEAR + if (yearsValue > maxYears) { + return `Maximum lock time is ${sliderConfig.maxLabel}` + } + + return null + } + + const builtInError = getValidationError() + const customError = onValidate ? onValidate(years, days) : null + const errorMessage = customError || builtInError + const displayError = externalErrorMessage || errorMessage + const hasError = Boolean(displayError) + + return ( +
+ + +
+
+ +
+ + or + +
+ +
+
+ + {hasError && ( +
+
+
+
+ +
+
+
+ {displayError} +
+
+
+ )} + +
+
+
+ Boost: +
+ + {/* TODO: calculate boost */} + x2.5 + +
+
+
+ Unlock: +
+ + {calculatedUnlockDate} + +
+
+ +
+
+
+
+ +
+
+
+ {infoMessage} +
+
+
+ +
+
+ + +
+
+ + ) +} diff --git a/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx b/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx new file mode 100644 index 000000000..a6c7821be --- /dev/null +++ b/apps/hub/src/app/_components/vaults/modals/withdraw-vault-modal.tsx @@ -0,0 +1,133 @@ +'use client' + +import { useCallback } from 'react' + +import * as Dialog from '@radix-ui/react-dialog' +import { InfoIcon } from '@status-im/icons/12' +import { Button } from '@status-im/status-network/components' +import { useAccount } from 'wagmi' + +import { useVaultWithdraw } from '~hooks/useVaultWithdraw' + +import { BaseVaultModal } from './base-vault-modal' + +import type { Address } from 'viem' + +interface WithdrawVaultModalProps { + onClose: () => void + vaultAddress: Address + open?: boolean + onOpenChange?: (open: boolean) => void + children?: React.ReactNode +} + +/** + * Modal for emergency withdrawal from vault + */ +export function WithdrawVaultModal(props: WithdrawVaultModalProps) { + const { onClose, vaultAddress, open, onOpenChange, children } = props + + const { address } = useAccount() + const { mutate: withdraw } = useVaultWithdraw() + + const handleVaultWithdrawal = useCallback(() => { + const amountWei = 1000000000000000000n + + if (!address) { + console.error('No address found - wallet not connected') + return + } + + try { + withdraw({ + amountWei, + vaultAddress, + onSigned: () => { + onClose() + }, + }) + } catch (error) { + console.error('Error calling withdraw:', error) + } + }, [address, onClose, vaultAddress, withdraw]) + + return ( + +
+
+
+
+

+ Withdraw to +

+
+
+
+

+ {address} +

+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+

+ Your funds will sent directly to your connected wallet. +

+
+ +
+
+
+
+ +
+
+ + + + +
+
+
+ ) +} diff --git a/apps/hub/src/app/_components/vaults/table-columns.tsx b/apps/hub/src/app/_components/vaults/table-columns.tsx new file mode 100644 index 000000000..7d8bb950e --- /dev/null +++ b/apps/hub/src/app/_components/vaults/table-columns.tsx @@ -0,0 +1,336 @@ +import { AlertIcon, TimeIcon } from '@status-im/icons/12' +import { LockedIcon, UnlockedIcon } from '@status-im/icons/20' +import { Button } from '@status-im/status-network/components' +import { createColumnHelper } from '@tanstack/react-table' +import { formatUnits } from 'viem' + +import { SNT_TOKEN } from '~constants/index' +import { type StakingVault } from '~hooks/useStakingVaults' +import { shortenAddress } from '~utils/address' +import { formatSNT } from '~utils/currency' +import { + calculateDaysUntilUnlock, + calculateVaultBoost, + isVaultLocked, +} from '~utils/vault' + +import { LockVaultModal } from './modals/lock-vault-modal' +import { WithdrawVaultModal } from './modals/withdraw-vault-modal' + +interface TableColumnsProps { + vaults: StakingVault[] | undefined + openModalVaultId: string | null + setOpenModalVaultId: (vaultId: string | null) => void + emergencyModeEnabled: unknown + isConnected: boolean +} + +export const createVaultTableColumns = ({ + vaults = [], + openModalVaultId, + setOpenModalVaultId, + emergencyModeEnabled, + isConnected, +}: TableColumnsProps) => { + const totalStaked = vaults.reduce( + (acc, vault) => acc + (vault.data?.stakedBalance || 0n), + BigInt(0) + ) + const totalKarma = vaults.reduce( + (acc, vault) => acc + (vault.data?.rewardsAccrued || 0n), + BigInt(0) + ) + const columnHelper = createColumnHelper() + + return [ + columnHelper.accessor('address', { + id: 'vault', + header: 'Vault', + cell: ({ row }) => { + return ( + + #{Number(row.index) + 1} + + ) + }, + footer: () => { + return ( + Total + ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('address', { + header: 'Address', + cell: ({ row }) => { + return ( + + {shortenAddress(row.original.address)} + + ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('data.stakedBalance', { + header: 'Staked', + cell: ({ row }) => { + return ( +
+ + {formatSNT(row.original.data?.stakedBalance || 0n)} + SNT + +
+ ) + }, + footer: () => { + return ( + + {formatSNT(totalStaked)} + SNT + + ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('data.lockUntil', { + header: 'Unlocks in', + cell: ({ row }) => { + const isLocked = isVaultLocked(row.original.data?.lockUntil) + const daysUntilUnlock = isLocked + ? calculateDaysUntilUnlock(row.original.data?.lockUntil) + : null + + if (!daysUntilUnlock) { + return null + } + + return ( +
+ + {daysUntilUnlock} + d + +
+ ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('data.maxMP', { + header: 'Boost', + cell: ({ row }) => { + const stakedBalance = row.original.data?.stakedBalance || 0n + const maxMP = row.original.data?.maxMP || 0n + const mpAccrued = row.original.data?.mpAccrued || 0n + + const potentialBoost = + stakedBalance > 0n && maxMP > mpAccrued + ? (maxMP - mpAccrued) / stakedBalance + : undefined + + return ( +
+ + x{calculateVaultBoost(vaults, row.original.address)} + + {potentialBoost && ( + + x{formatSNT(formatUnits(potentialBoost, SNT_TOKEN.decimals))} if + locked + + )} +
+ ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('data.rewardsAccrued', { + header: 'Karma', + cell: ({ row }) => { + const karma = Number(row.original.data?.rewardsAccrued) / 1e18 + return ( +
+ + {formatSNT(karma)} + KARMA + +
+ ) + }, + footer: () => { + return ( + + {formatSNT(totalKarma)} + KARMA + + ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.accessor('data.lockUntil', { + id: 'state', + header: 'State', + cell: ({ row }) => { + const isLocked = isVaultLocked(row.original.data?.lockUntil) + + return ( +
+ {isLocked ? ( + + ) : ( + + )} + + {isLocked ? 'Locked' : 'Open'} + +
+ ) + }, + meta: { + headerClassName: 'text-left', + cellClassName: 'text-left', + }, + }), + columnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ row }) => { + const withdrawModalId = `withdraw-${row.original.address}` + const lockModalId = `lock-${row.original.address}` + const isWithdrawModalOpen = openModalVaultId === withdrawModalId + const isLockModalOpen = openModalVaultId === lockModalId + + const isLocked = row.original?.data?.lockUntil + ? row.original.data.lockUntil > BigInt(Math.floor(Date.now() / 1000)) + : false + + return ( +
+ {isLocked ? ( +
+ {!emergencyModeEnabled && ( + + setOpenModalVaultId(open ? withdrawModalId : null) + } + onClose={() => setOpenModalVaultId(null)} + vaultAddress={row.original.address} + > + + + )} + + setOpenModalVaultId(open ? lockModalId : null) + } + vaultAddress={row.original.address} + title="Extend lock time" + initialYears="2" + initialDays="732" + description="Extending lock time increasing Karma boost" + actions={[ + { + label: 'Cancel', + }, + { + label: 'Extend lock', + }, + ]} + onClose={() => setOpenModalVaultId(null)} + infoMessage="Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks." + > + + +
+ ) : ( + + setOpenModalVaultId(open ? lockModalId : null) + } + vaultAddress={row.original.address} + title="Do you want to lock the vault?" + description="Lock this vault to receive more Karma" + actions={[ + { + label: "Don't lock", + }, + { + label: 'Lock', + }, + ]} + onClose={() => setOpenModalVaultId(null)} + infoMessage="Boost the rate at which you receive Karma. The longer you lock your vault, the higher your boost, and the faster you accumulate Karma. You can add more SNT at any time, but withdrawing your SNT is only possible once the vault unlocks." + onValidate={(_, days) => { + const totalDays = parseInt(days || '0') + // TODO: read this from the contract + return totalDays > 1460 + ? 'Maximum lock time is 4 years' + : null + }} + > + + + )} +
+ ) + }, + meta: { + headerClassName: 'text-center', + cellClassName: 'text-right', + }, + }), + ] +} diff --git a/apps/hub/src/app/_components/vaults/vaults-table.tsx b/apps/hub/src/app/_components/vaults/vaults-table.tsx new file mode 100644 index 000000000..3502fecc8 --- /dev/null +++ b/apps/hub/src/app/_components/vaults/vaults-table.tsx @@ -0,0 +1,184 @@ +'use client' + +import { useMemo, useState } from 'react' + +import { AddCircleIcon } from '@status-im/icons/12' +import { Button } from '@status-im/status-network/components' +import { + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' +import { useAccount, useReadContract } from 'wagmi' + +import { STAKING_MANAGER } from '~constants/index' +import { useCreateVault } from '~hooks/useCreateVault' +import { type StakingVault, useStakingVaults } from '~hooks/useStakingVaults' + +import { createVaultTableColumns } from './table-columns' + +interface VaultColumnMeta { + headerClassName?: string + cellClassName?: string + footerClassName?: string +} + +/** + * Gets the appropriate CSS class for table cell/header based on meta + */ +function getCellClassName( + meta: VaultColumnMeta | undefined, + defaultClassName = 'text-left' +): string { + return meta?.cellClassName || defaultClassName +} + +function getHeaderClassName( + meta: VaultColumnMeta | undefined, + defaultClassName = 'text-left' +): string { + return meta?.headerClassName || defaultClassName +} + +// ============================================================================ +// Sub-components +// ============================================================================ + +interface TableProps { + table: ReturnType> +} + +function TableHeader({ table }: TableProps) { + return ( + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + ))} + + ))} + + ) +} + +function TableBody({ table }: TableProps) { + return ( + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + ) +} + +function TableFooter({ table }: TableProps) { + return ( + + {table.getFooterGroups().map(footerGroup => ( + + {footerGroup.headers.map(header => ( + + {header.column.columnDef.footer + ? flexRender( + header.column.columnDef.footer, + header.getContext() + ) + : null} + + ))} + + ))} + + ) +} + +// ============================================================================ +// Main Component +// ============================================================================ + +export function VaultsTable() { + const [openModalVaultId, setOpenModalVaultId] = useState(null) + const { data: vaultDataList } = useStakingVaults() + const { isConnected } = useAccount() + const { mutate: createVault } = useCreateVault() + + const { data: emergencyModeEnabled } = useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'emergencyModeEnabled', + }) + + const columns = useMemo( + () => + createVaultTableColumns({ + vaults: vaultDataList, + openModalVaultId, + setOpenModalVaultId, + emergencyModeEnabled, + isConnected, + }), + [vaultDataList, openModalVaultId, emergencyModeEnabled, isConnected] + ) + + const table = useReactTable({ + data: vaultDataList ?? [], + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+
+

+ My vaults +

+ {isConnected && ( + + )} +
+ +
+
+
+ + + + +
+
+
+
+
+ ) +} diff --git a/apps/hub/src/app/_constants/address.ts b/apps/hub/src/app/_constants/address.ts new file mode 100644 index 000000000..20a81cee3 --- /dev/null +++ b/apps/hub/src/app/_constants/address.ts @@ -0,0 +1,31 @@ +import { + faucetAbi, + stakingManagerAbi, + tokenAbi, + vaultFactoryAbi, +} from './contracts' + +import type { Abi, Address } from 'viem' + +export const STAKING_MANAGER = { + address: '0x5cDf1646E4c1D21eE94DED1DA8da3Ca450dc96D1' as Address, + abi: stakingManagerAbi as Abi, +} as const + +export const VAULT_FACTORY = { + address: '0xddDcd43a0B0dA865decf3e4Ae71FbBE3e2DfFF14' as Address, + abi: vaultFactoryAbi as Abi, +} as const + +export const SNT_TOKEN = { + address: '0x1C3Ac2a186c6149Ae7Cb4D716eBbD0766E4f898a' as Address, + name: 'Status Test Token', + symbol: 'STT', + decimals: 18, + abi: tokenAbi as Abi, +} as const + +export const FAUCET = { + address: '0x4Fb609F4a457f47B41D35Dd060447271F000120A' as Address, + abi: faucetAbi as Abi, +} as const diff --git a/apps/hub/src/app/_constants/chain.ts b/apps/hub/src/app/_constants/chain.ts new file mode 100644 index 000000000..f97d3dd93 --- /dev/null +++ b/apps/hub/src/app/_constants/chain.ts @@ -0,0 +1,25 @@ +import type { Chain } from 'wagmi/chains' + +export const statusNetworkTestnet: Chain = { + id: 1660990954, + name: 'Status Network Testnet', + nativeCurrency: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + rpcUrls: { + default: { + http: ['https://public.sepolia.rpc.status.network'], + }, + public: { + http: ['https://public.sepolia.rpc.status.network'], + }, + }, + blockExplorers: { + default: { + name: 'Status Explorer', + url: 'https://sepoliascan.status.network', + }, + }, +} diff --git a/apps/hub/src/app/_constants/contracts/faucet.ts b/apps/hub/src/app/_constants/contracts/faucet.ts new file mode 100644 index 000000000..36184e86a --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/faucet.ts @@ -0,0 +1,48 @@ +export const faucetAbi = [ + { + inputs: [{ internalType: 'address', name: '_token', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'DailyLimitExceeded', type: 'error' }, + { inputs: [], name: 'InvalidAddress', type: 'error' }, + { inputs: [], name: 'InvalidAmount', type: 'error' }, + { + inputs: [], + name: 'DAILY_LIMIT', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'accountDailyRequests', + outputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'accountResetTime', + outputs: [{ internalType: 'uint256', name: 'time', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'address', name: 'receiver', type: 'address' }, + ], + name: 'requestTokens', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'token', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] diff --git a/apps/hub/src/app/_constants/contracts/index.ts b/apps/hub/src/app/_constants/contracts/index.ts new file mode 100644 index 000000000..4af6b2a90 --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/index.ts @@ -0,0 +1,5 @@ +export { faucetAbi } from './faucet' +export { stakingManagerAbi } from './stakingManagerAbi' +export { tokenAbi } from './tokenAbi' +export { vaultAbi } from './vaultAbi' +export { vaultFactoryAbi } from './vaultFactoryAbi' diff --git a/apps/hub/src/app/_constants/contracts/stakingManagerAbi.ts b/apps/hub/src/app/_constants/contracts/stakingManagerAbi.ts new file mode 100644 index 000000000..51e592803 --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/stakingManagerAbi.ts @@ -0,0 +1,992 @@ +export const stakingManagerAbi = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { inputs: [], name: 'StakeManager__AmountCannotBeZero', type: 'error' }, + { inputs: [], name: 'StakeManager__DurationCannotBeZero', type: 'error' }, + { inputs: [], name: 'StakeManager__EmergencyModeEnabled', type: 'error' }, + { inputs: [], name: 'StakeManager__FundsLocked', type: 'error' }, + { inputs: [], name: 'StakeManager__InsufficientBalance', type: 'error' }, + { inputs: [], name: 'StakeManager__InvalidVault', type: 'error' }, + { inputs: [], name: 'StakeManager__MigrationTargetHasFunds', type: 'error' }, + { inputs: [], name: 'StakeManager__RewardPeriodNotEnded', type: 'error' }, + { inputs: [], name: 'StakeManager__RewardTransferFailed', type: 'error' }, + { inputs: [], name: 'StakeManager__Unauthorized', type: 'error' }, + { inputs: [], name: 'StakeManager__VaultAlreadyRegistered', type: 'error' }, + { inputs: [], name: 'StakeManager__VaultNotRegistered', type: 'error' }, + { inputs: [], name: 'StakeMath__AbsoluteMaxMPOverflow', type: 'error' }, + { inputs: [], name: 'StakeMath__FundsLocked', type: 'error' }, + { inputs: [], name: 'StakeMath__InsufficientBalance', type: 'error' }, + { inputs: [], name: 'StakeMath__InvalidAmount', type: 'error' }, + { inputs: [], name: 'StakeMath__InvalidLockingPeriod', type: 'error' }, + { + inputs: [], + name: 'TrustedCodehashAccess__UnauthorizedCodehash', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'previousAdmin', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'AdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'beacon', + type: 'address', + }, + ], + name: 'BeaconUpgraded', + type: 'event', + }, + { anonymous: false, inputs: [], name: 'EmergencyModeEnabled', type: 'event' }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint8', name: 'version', type: 'uint8' }, + ], + name: 'Initialized', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'lockPeriod', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'lockUntil', + type: 'uint256', + }, + ], + name: 'Locked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'duration', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'endTime', + type: 'uint256', + }, + ], + name: 'RewardSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'RewardsRedeemed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'supplier', + type: 'address', + }, + ], + name: 'RewardsSupplierSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'bytes32', + name: 'previousAdminRole', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'newAdminRole', + type: 'bytes32', + }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'lockTime', + type: 'uint256', + }, + ], + name: 'StakeMathTest', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'lockPeriod', + type: 'uint256', + }, + ], + name: 'Staked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'codehash', + type: 'bytes32', + }, + { indexed: false, internalType: 'bool', name: 'trusted', type: 'bool' }, + ], + name: 'TrustedCodehashUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Unstaked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'implementation', + type: 'address', + }, + ], + name: 'Upgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + ], + name: 'VaultLeft', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + ], + name: 'VaultMigrated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'VaultRegistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'rewardsAccrued', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'mpAccrued', + type: 'uint256', + }, + ], + name: 'VaultUpdated', + type: 'event', + }, + { + inputs: [], + name: 'ACCRUE_RATE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'GUARDIAN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MAX_BALANCE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MAX_LOCKUP_PERIOD', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MAX_MULTIPLIER', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MIN_BALANCE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MIN_LOCKUP_PERIOD', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MP_APY', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MP_MPY', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MP_MPY_ABSOLUTE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REWARD_TOKEN', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'SCALE_FACTOR', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'STAKING_TOKEN', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'YEAR', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_initialOwner', type: 'address' }, + ], + name: '__TrustedCodehashAccess_init', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'emergencyModeEnabled', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'enableEmergencyMode', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getAccountTotalMaxMP', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getAccountTotalStakedBalance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getAccountVaults', + outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'getVault', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'stakedBalance', type: 'uint256' }, + { internalType: 'uint256', name: 'rewardIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'mpAccrued', type: 'uint256' }, + { internalType: 'uint256', name: 'maxMP', type: 'uint256' }, + { + internalType: 'uint256', + name: 'lastMPUpdateTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'rewardsAccrued', type: 'uint256' }, + ], + internalType: 'struct StakeManager.VaultData', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_owner', type: 'address' }, + { internalType: 'address', name: '_stakingToken', type: 'address' }, + { internalType: 'address', name: '_rewardToken', type: 'address' }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '_codehash', type: 'bytes32' }], + name: 'isTrustedCodehash', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'lastMPUpdatedTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'lastRewardIndex', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'lastRewardTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'leave', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'lockPeriod', type: 'uint256' }, + { internalType: 'uint256', name: 'currentLockUntil', type: 'uint256' }, + ], + name: 'lock', + outputs: [ + { internalType: 'uint256', name: 'newLockUntil', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'migrateTo', type: 'address' }], + name: 'migrateToVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'mpAccruedOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'mpBalanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'mpBalanceOfAccount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'proxiableUUID', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'redeemRewards', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'registerVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'rewardAmount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'rewardEndTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'rewardStartTime', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'rewardsBalanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'rewardsBalanceOfAccount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'rewardsSupplier', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + ], + name: 'setReward', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_rewardsSupplier', type: 'address' }, + ], + name: 'setRewardsSupplier', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: '_codehash', type: 'bytes32' }, + { internalType: 'bool', name: '_trusted', type: 'bool' }, + ], + name: 'setTrustedCodehash', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'lockPeriod', type: 'uint256' }, + { internalType: 'uint256', name: 'currentLockUntil', type: 'uint256' }, + ], + name: 'stake', + outputs: [ + { internalType: 'uint256', name: 'newLockUntil', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'stakedBalanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalMP', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalMPAccrued', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalMPStaked', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalMaxMP', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalRewardsAccrued', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalRewardsSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalShares', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalStaked', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'unpause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'unstake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'updateAccount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'updateGlobalState', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'updateVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'newImplementation', type: 'address' }, + ], + name: 'upgradeTo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'newImplementation', type: 'address' }, + { internalType: 'bytes', name: 'data', type: 'bytes' }, + ], + name: 'upgradeToAndCall', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'vault', type: 'address' }], + name: 'vaultData', + outputs: [ + { internalType: 'uint256', name: 'stakedBalance', type: 'uint256' }, + { internalType: 'uint256', name: 'rewardIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'mpAccrued', type: 'uint256' }, + { internalType: 'uint256', name: 'maxMP', type: 'uint256' }, + { internalType: 'uint256', name: 'lastMPUpdateTime', type: 'uint256' }, + { internalType: 'uint256', name: 'rewardsAccrued', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'vault', type: 'address' }], + name: 'vaultOwners', + outputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'vaultAddress', type: 'address' }, + ], + name: 'vaultShares', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'uint256', name: '', type: 'uint256' }, + ], + name: 'vaults', + outputs: [{ internalType: 'address', name: 'vault', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] diff --git a/apps/hub/src/app/_constants/contracts/tokenAbi.ts b/apps/hub/src/app/_constants/contracts/tokenAbi.ts new file mode 100644 index 000000000..fa5ea247d --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/tokenAbi.ts @@ -0,0 +1,60 @@ +export const tokenAbi = [ + { + inputs: [{ internalType: 'address', name: '_owner', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: 'balance', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_spender', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_owner', type: 'address' }, + { internalType: 'address', name: '_spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: 'remaining', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_to', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/apps/hub/src/app/_constants/contracts/vaultAbi.ts b/apps/hub/src/app/_constants/contracts/vaultAbi.ts new file mode 100644 index 000000000..f9b8cb975 --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/vaultAbi.ts @@ -0,0 +1,258 @@ +import type { Abi } from 'viem' + +export const vaultAbi: Abi = [ + { + inputs: [ + { internalType: 'contract IERC20', name: 'token', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'StakeVault__FundsLocked', type: 'error' }, + { inputs: [], name: 'StakeVault__InvalidDestinationAddress', type: 'error' }, + { inputs: [], name: 'StakeVault__MigrationFailed', type: 'error' }, + { inputs: [], name: 'StakeVault__NotAllowedToExit', type: 'error' }, + { inputs: [], name: 'StakeVault__NotAllowedToLeave', type: 'error' }, + { inputs: [], name: 'StakeVault__NotAuthorized', type: 'error' }, + { inputs: [], name: 'StakeVault__NotEnoughAvailableBalance', type: 'error' }, + { + inputs: [], + name: 'StakeVault__StakeManagerImplementationNotTrusted', + type: 'error', + }, + { inputs: [], name: 'StakeVault__StakingFailed', type: 'error' }, + { inputs: [], name: 'StakeVault__UnstakingFailed', type: 'error' }, + { inputs: [], name: 'StakeVault__WithdrawFromVaultFailed', type: 'error' }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint8', name: 'version', type: 'uint8' }, + ], + name: 'Initialized', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + inputs: [], + name: 'STAKING_TOKEN', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'amountStaked', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: '_token', type: 'address' }, + ], + name: 'availableWithdraw', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_destination', type: 'address' }, + ], + name: 'emergencyExit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_owner', type: 'address' }, + { internalType: 'address', name: '_stakeManager', type: 'address' }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_destination', type: 'address' }, + ], + name: 'leave', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_seconds', type: 'uint256' }], + name: 'lock', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'lockUntil', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'migrateTo', type: 'address' }], + name: 'migrateToVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'register', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint256', name: '_seconds', type: 'uint256' }, + { internalType: 'address', name: '_from', type: 'address' }, + ], + name: 'stake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint256', name: '_seconds', type: 'uint256' }, + ], + name: 'stake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'stakeManager', + outputs: [ + { + internalType: 'contract IStakeManagerProxy', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'stakeManagerImplementationAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'stakeManagerAddress', type: 'address' }, + ], + name: 'trustStakeManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'unstake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'address', name: '_destination', type: 'address' }, + ], + name: 'unstake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_lockUntil', type: 'uint256' }], + name: 'updateLockUntil', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: '_token', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'address', name: '_destination', type: 'address' }, + ], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'contract IERC20', name: '_token', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + ], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'address', name: '_destination', type: 'address' }, + ], + name: 'withdrawFromVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] diff --git a/apps/hub/src/app/_constants/contracts/vaultFactoryAbi.ts b/apps/hub/src/app/_constants/contracts/vaultFactoryAbi.ts new file mode 100644 index 000000000..7648ad0a3 --- /dev/null +++ b/apps/hub/src/app/_constants/contracts/vaultFactoryAbi.ts @@ -0,0 +1,150 @@ +export const vaultFactoryAbi = [ + { + inputs: [ + { internalType: 'address', name: '_owner', type: 'address' }, + { internalType: 'address', name: '_stakeManager', type: 'address' }, + { + internalType: 'address', + name: '_vaultImplementation', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [], + name: 'VaultFactory__InvalidStakeManagerAddress', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'newStakeManagerAddress', + type: 'address', + }, + ], + name: 'StakeManagerAddressChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'vault', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'VaultCreated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'newVaultImplementation', + type: 'address', + }, + ], + name: 'VaultImplementationChanged', + type: 'event', + }, + { + inputs: [], + name: 'createVault', + outputs: [ + { internalType: 'contract StakeVault', name: 'clone', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_stakeManager', type: 'address' }, + ], + name: 'setStakeManager', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_vaultImplementation', + type: 'address', + }, + ], + name: 'setVaultImplementation', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'stakeManager', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'vaultImplementation', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] diff --git a/apps/hub/src/app/_constants/index.ts b/apps/hub/src/app/_constants/index.ts new file mode 100644 index 000000000..6b4627b25 --- /dev/null +++ b/apps/hub/src/app/_constants/index.ts @@ -0,0 +1,2 @@ +export * from './address' +export * from './chain' diff --git a/apps/hub/src/app/_hooks/useApproveToken.ts b/apps/hub/src/app/_hooks/useApproveToken.ts new file mode 100644 index 000000000..8973b2945 --- /dev/null +++ b/apps/hub/src/app/_hooks/useApproveToken.ts @@ -0,0 +1,175 @@ +import { useToast } from '@status-im/components' +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { type Address, parseUnits, zeroAddress } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { SNT_TOKEN } from '~constants/index' +import { useVaultStateContext } from '~hooks/useVaultStateContext' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Parameters for token approval + */ +export interface ApproveTokenParams { + /** Amount of tokens to approve (in token units, not wei) */ + amount: string + /** Address of the spender (vault) to approve tokens for */ + spenderAddress: Address +} + +/** + * Return type for useApproveToken hook + */ +export type UseApproveTokenReturn = UseMutationResult< + void, + Error, + ApproveTokenParams, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY = 'approve-token' as const + +const TRANSACTION_CONFIG = { + CONFIRMATION_BLOCKS: 1, +} as const + +// ============================================================================ +// Hook +// ============================================================================ + +/** + * Mutation hook to approve SNT tokens for a spender (typically a vault) + * + * **Approval Process:** + * 1. Validates wallet connection and parameters + * 2. Converts amount to wei + * 3. Calls ERC20 approve function + * 4. Waits for transaction confirmation + * 5. Updates state machine with approval status + * + * @returns Mutation result with approval function and status + * + * @throws {Error} When wallet is not connected + * @throws {Error} When spender address is invalid + * @throws {Error} When amount is invalid or zero + * @throws {Error} When transaction is reverted + * + * @example + * Basic usage + * ```tsx + * function ApproveButton() { + * const { mutate: approveToken, isPending } = useApproveToken() + * + * const handleApprove = () => { + * approveToken({ + * amount: '100', + * spenderAddress: vaultAddress + * }) + * } + * + * return ( + * + * ) + * } + * ``` + * + * @example + * With callbacks + * ```tsx + * approveToken( + * { amount: '100', spenderAddress: vaultAddress }, + * { + * onSuccess: (txHash) => { + * console.log('Approved! Transaction:', txHash) + * }, + * onError: (error) => { + * console.error('Approval failed:', error) + * }, + * } + * ) + * ``` + */ +export function useApproveToken(): UseApproveTokenReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const { send: sendVaultEvent } = useVaultStateContext() + const toast = useToast() + + return useMutation({ + mutationKey: [MUTATION_KEY, address], + mutationFn: async ({ + amount, + spenderAddress, + }: ApproveTokenParams): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Validate spender address + if (!spenderAddress || spenderAddress === zeroAddress) { + throw new Error('Invalid spender address provided') + } + + // Validate amount + if (!amount || parseFloat(amount) <= 0) { + throw new Error('Amount must be greater than 0') + } + + // Convert amount to wei + const amountWei = parseUnits(amount, SNT_TOKEN.decimals) + + // Notify state machine of approval start + sendVaultEvent({ + type: 'START_INCREASE_ALLOWANCE', + amount, + }) + + try { + // Execute approval transaction + const hash = await writeContractAsync({ + address: SNT_TOKEN.address, + abi: SNT_TOKEN.abi, + functionName: 'approve', + args: [spenderAddress, amountWei], + }) + + // Transaction submitted, notify state machine + sendVaultEvent({ type: 'SIGN' }) + + // Wait for confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: TRANSACTION_CONFIG.CONFIRMATION_BLOCKS, + }) + + // Check for revert + if (status === 'reverted') { + throw new Error('Transaction was reverted') + } + + // Approval successful, notify state machine + sendVaultEvent({ type: 'COMPLETE', amount }) + toast.positive('Token Allowance has been increased') + } catch (error) { + // Handle approval failure + console.error('Failed to approve tokens:', error) + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useCompoundMultiplierPoints.ts b/apps/hub/src/app/_hooks/useCompoundMultiplierPoints.ts new file mode 100644 index 000000000..0e627f3eb --- /dev/null +++ b/apps/hub/src/app/_hooks/useCompoundMultiplierPoints.ts @@ -0,0 +1,170 @@ +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { formatUnits, zeroAddress } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { + SNT_TOKEN, + STAKING_MANAGER, + statusNetworkTestnet, +} from '~constants/index' +import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints' + +import { useVaultStateContext } from './useVaultStateContext' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Return type for the useCompoundMultiplierPoints mutation hook + */ +export type UseCompoundMultiplierPointsReturn = UseMutationResult< + void, + Error, + void, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY = 'compound-multiplier-points' as const + +const TRANSACTION_CONFIG = { + CONFIRMATION_BLOCKS: 1, +} as const + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to compound multiplier points (MP) for the connected account + * + * **Compounding Process:** + * Calls StakingManager.updateAccount() which: + * - Updates global state (reward index, MP accrual) + * - Calculates new MP accrued since last update + * - Updates the account's reward index + * - Compounds rewards based on current MP balance + * - Emits AccountUpdated event + * + * @returns Mutation result with mutate function to trigger compounding + * + * @throws {Error} When wallet is not connected + * @throws {Error} When account address is invalid + * @throws {Error} When transaction is reverted + * + * @example + * Basic usage + * ```tsx + * function CompoundButton() { + * const { mutate: compound, isPending } = useCompoundMultiplierPoints() + * + * return ( + * + * ) + * } + * ``` + * + * @example + * With success/error handling + * ```tsx + * function CompoundManager() { + * const { mutate: compound, isPending } = useCompoundMultiplierPoints() + * + * const handleCompound = () => { + * compound(undefined, { + * onSuccess: (txHash) => { + * toast.success(`Compounded! Tx: ${txHash}`) + * }, + * onError: (error) => { + * toast.error(`Failed to compound: ${error.message}`) + * }, + * }) + * } + * + * return ( + * + * ) + * } + * ``` + */ +export function useCompoundMultiplierPoints(): UseCompoundMultiplierPointsReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const { data: mpBalance, refetch: refetchMultiplierPoints } = + useMultiplierPointsBalance() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + + return useMutation({ + mutationKey: [MUTATION_KEY, address], + mutationFn: async (): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Validate account address + if (address === zeroAddress) { + throw new Error('Invalid account address provided') + } + + // Get the current MP balance to show in the dialog + const formattedAmount = formatUnits( + mpBalance?.totalUncompounded || 0n, + SNT_TOKEN.decimals + ) + + sendVaultEvent({ + type: 'START_COMPOUND', + amount: formattedAmount, + }) + + // Execute compound transaction via updateAccount + const hash = await writeContractAsync({ + chain: statusNetworkTestnet, + account: address, + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'updateAccount', + args: [address], + }) + + // Signal user to sign transaction + sendVaultEvent({ type: 'SIGN' }) + + // Wait for transaction confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: TRANSACTION_CONFIG.CONFIRMATION_BLOCKS, + }) + + // Check if transaction was reverted + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + }, + onSuccess: () => { + resetVault() + // Refetch account vaults to update UI with new compounded values + refetchMultiplierPoints() + }, + onError: () => { + sendVaultEvent({ type: 'REJECT' }) + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useCreateVault.ts b/apps/hub/src/app/_hooks/useCreateVault.ts new file mode 100644 index 000000000..e5d7725fa --- /dev/null +++ b/apps/hub/src/app/_hooks/useCreateVault.ts @@ -0,0 +1,186 @@ +import { useToast } from '@status-im/components' +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { type Address, type Hash } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { VAULT_FACTORY } from '~constants/index' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useVaultStateContext } from '~hooks/useVaultStateContext' +import { shortenAddress } from '~utils/address' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Return type for the useCreateVault hook + */ +export type UseCreateVaultReturn = UseMutationResult + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY = 'create-vault' as const + +const DEFAULT_DELAY = 100 as const + +const TRANSACTION_CONFIG = { + CONFIRMATION_BLOCKS: 1, +} as const + +// Event signature for VaultCreated(address indexed vault, address indexed owner) +const VAULT_CREATED_EVENT_SIGNATURE = + '0x5d9c31ffa0fecffd7cf379989a3c7af252f0335e0d2a1320b55245912c781f53' as const + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Extracts the vault address from VaultCreated event logs + */ +function extractVaultAddressFromLogs( + logs: Array<{ address: Address; topics: readonly string[] }> +): Address { + const vaultCreatedLog = logs.find(log => { + // Check if this log is from the vault factory contract + if (log.address.toLowerCase() !== VAULT_FACTORY.address.toLowerCase()) { + return false + } + + // Check if this is the VaultCreated event + return log.topics[0] === VAULT_CREATED_EVENT_SIGNATURE + }) + + if (!vaultCreatedLog || !vaultCreatedLog.topics[1]) { + throw new Error('VaultCreated event not found in transaction logs') + } + + // The vault address is the first indexed parameter (topics[1]) + // Remove the padding from the address (first 24 bytes) + const paddedAddress = vaultCreatedLog.topics[1] as `0x${string}` + return `0x${paddedAddress.slice(26)}` as Address +} + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to create a new staking vault + * + * **Creation Process:** + * 1. Calls VaultFactory.createVault() + * 2. Waits for transaction confirmation + * 3. Extracts the new vault address from event logs + * 4. Updates state machine and refetches vaults + * 5. Shows success toast notification + * + * @returns Mutation result with mutate function to trigger vault creation + * + * @throws {Error} When wallet is not connected + * @throws {Error} When transaction is reverted + * @throws {Error} When vault address cannot be extracted + * + * @example + * Basic usage + * ```tsx + * function CreateVaultButton() { + * const { mutate: createVault, isPending } = useCreateVault() + * + * return ( + * + * ) + * } + * ``` + * + * @example + * With success handling + * ```tsx + * const { mutate: createVault } = useCreateVault() + * + * createVault(undefined, { + * onSuccess: (txHash) => { + * console.log('Vault created! Transaction:', txHash) + * }, + * onError: (error) => { + * console.error('Failed to create vault:', error) + * }, + * }) + * ``` + */ +export function useCreateVault(): UseCreateVaultReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const { refetch: refetchStakingVaults } = useStakingVaults() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + const config = useConfig() + const toast = useToast() + + return useMutation({ + mutationKey: [MUTATION_KEY, address], + mutationFn: async (): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Notify state machine of vault creation start + sendVaultEvent({ type: 'START_CREATE_VAULT' }) + + try { + // Execute vault creation transaction + const hash = await writeContractAsync({ + address: VAULT_FACTORY.address, + abi: VAULT_FACTORY.abi, + functionName: 'createVault', + }) + + // Transaction submitted, notify state machine + sendVaultEvent({ type: 'SIGN' }) + + // Wait for transaction confirmation and get logs + const { status, logs } = await waitForTransactionReceipt(config, { + hash, + confirmations: TRANSACTION_CONFIG.CONFIRMATION_BLOCKS, + }) + + // Check for transaction revert + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + + // Extract the newly created vault address from logs + const deployedVaultAddress = extractVaultAddressFromLogs(logs) + + // Show success toast + toast.positive( + `Vault ${shortenAddress(deployedVaultAddress)} has been created` + ) + + // Small delay to ensure toast is rendered before state reset + await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY)) + + // Reset state machine and refetch vaults + resetVault() + await refetchStakingVaults() + + return hash + } catch (error) { + // Transaction failed or user rejected + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useExchangeRate.ts b/apps/hub/src/app/_hooks/useExchangeRate.ts new file mode 100644 index 000000000..a59a68441 --- /dev/null +++ b/apps/hub/src/app/_hooks/useExchangeRate.ts @@ -0,0 +1,144 @@ +import { useQuery } from '@tanstack/react-query' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Exchange rate data for SNT/USDT pair + */ +export interface ExchangeRateData { + /** Exchange rate price (SNT in USDT) */ + price: number + /** Timestamp when the rate was fetched */ + timestamp: number +} + +/** + * Options for exchange rate query configuration + */ +export interface UseExchangeRateOptions { + /** + * Enable or disable the query + * @default true + */ + enabled?: boolean + /** + * Refetch interval in milliseconds + * @default 60000 (1 minute) + */ + refetchInterval?: number + /** + * Time in milliseconds until data is considered stale + * @default 30000 (30 seconds) + */ + staleTime?: number +} + +/** + * Return type for useExchangeRate hook + */ +export type UseExchangeRateReturn = ReturnType + +// ============================================================================ +// Constants +// ============================================================================ + +const QUERY_KEY_PREFIX = 'exchangeRate' +const BINANCE_API_URL = + 'https://api.binance.com/api/v3/ticker/price?symbol=SNTUSDT' +const DEFAULT_REFETCH_INTERVAL = 60_000 // 1 minute +const DEFAULT_STALE_TIME = 30_000 // 30 seconds + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Fetches the current SNT/USDT exchange rate from Binance + * + * @returns Exchange rate data with price and timestamp + * @throws Error if the API request fails or returns invalid data + */ +async function fetchExchangeRate(): Promise { + const response = await fetch(BINANCE_API_URL) + + if (!response.ok) { + throw new Error( + `Failed to fetch exchange rate: ${response.status} ${response.statusText}` + ) + } + + const data = await response.json() + + if (!data.price) { + throw new Error('Invalid response from Binance API: missing price') + } + + const price = parseFloat(data.price) + + if (isNaN(price)) { + throw new Error(`Invalid price value: ${data.price}`) + } + + return { + price, + timestamp: Date.now(), + } +} + +// ============================================================================ +// Hook +// ============================================================================ + +/** + * Hook to fetch and cache the current SNT/USDT exchange rate + * + * Automatically refetches the rate at regular intervals to keep data fresh. + * Provides loading, error, and success states via React Query. + * + * @param options - Query configuration options + * @returns React Query result with exchange rate data + * + * @example + * ```tsx + * function PriceDisplay() { + * const { data, isLoading, error } = useExchangeRate() + * + * if (isLoading) return
Loading price...
+ * if (error) return
Failed to load price
+ * + * return
1 SNT = ${data.price.toFixed(6)} USDT
+ * } + * ``` + * + * @example + * ```tsx + * // Custom refetch interval + * function LivePrice() { + * const { data } = useExchangeRate({ + * refetchInterval: 30000, // Refetch every 30 seconds + * }) + * + * return
${data?.price.toFixed(6)}
+ * } + * ``` + */ +export function useExchangeRate(options: UseExchangeRateOptions = {}) { + const { + enabled = true, + refetchInterval = DEFAULT_REFETCH_INTERVAL, + staleTime = DEFAULT_STALE_TIME, + } = options + + return useQuery({ + queryKey: [QUERY_KEY_PREFIX] as const, + queryFn: fetchExchangeRate, + enabled, + refetchInterval, + staleTime, + retry: 3, + retryDelay: attemptIndex => + Math.min(1000 * 2 ** attemptIndex, DEFAULT_STALE_TIME), + }) +} diff --git a/apps/hub/src/app/_hooks/useFaucet.ts b/apps/hub/src/app/_hooks/useFaucet.ts new file mode 100644 index 000000000..c23ffa171 --- /dev/null +++ b/apps/hub/src/app/_hooks/useFaucet.ts @@ -0,0 +1,250 @@ +import { + useMutation, + type UseMutationResult, + useQuery, + useQueryClient, + type UseQueryResult, +} from '@tanstack/react-query' +import { useAccount, useChainId, useConfig, useWriteContract } from 'wagmi' +import { readContracts } from 'wagmi/actions' + +import { FAUCET, statusNetworkTestnet } from '~constants/index' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Faucet data returned from contract queries + */ +export interface FaucetData { + /** Daily token request limit for each account */ + dailyLimit: bigint + /** Number of requests made by the account today */ + accountDailyRequests: bigint + /** Unix timestamp when the account's daily limit resets */ + accountResetTime: bigint +} + +/** + * Derived faucet state information + */ +export interface FaucetState extends FaucetData { + /** Whether the account can claim tokens (hasn't hit daily limit) */ + canClaim: boolean + /** Number of requests remaining for the account today */ + remainingRequests: bigint + /** Whether the account has used the faucet today */ + hasUsedFaucet: boolean + /** Amount of tokens actually used today */ + actualUsedToday: bigint + /** Amount of tokens remaining for the account today */ + remainingAmount: bigint +} + +/** + * Options for the faucet query hook + */ +export interface UseFaucetQueryOptions { + /** Whether to enable the query. Defaults to when address is available */ + enabled?: boolean + /** Refetch interval in milliseconds */ + refetchInterval?: number +} + +export interface UseFaucetMutationOptions { + amount?: bigint +} + +// ============================================================================ +// Constants +// ============================================================================ + +const QUERY_KEY_PREFIX = 'faucet' as const +const DEFAULT_REFETCH_INTERVAL = 30_000 // 30 seconds + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to request tokens from the faucet + * + * @returns Mutation result with requestTokens function + * + * @throws {Error} When wallet is not connected + * + * @example + * ```tsx + * function FaucetButton() { + * const { mutate: requestTokens, isPending } = useFaucetMutation() + * + * return ( + * + * ) + * } + * ``` + */ +export function useFaucetMutation(): UseMutationResult< + void, + Error, + UseFaucetMutationOptions, + unknown +> { + const { address } = useAccount() + const { writeContract } = useWriteContract() + const chainId = useChainId() + const queryClient = useQueryClient() + const { refetch: refetchFaucetQuery } = useFaucetQuery() + + return useMutation({ + mutationKey: [QUERY_KEY_PREFIX, 'request', address], + mutationFn: async ({ amount }: UseFaucetMutationOptions) => { + if (!address) { + throw new Error('Wallet not connected') + } + + return writeContract({ + chain: statusNetworkTestnet, + account: address, + address: FAUCET.address, + abi: FAUCET.abi, + functionName: 'requestTokens', + args: [amount ?? 0n, address], + }) + }, + onSuccess: () => { + refetchFaucetQuery() + // Invalidate faucet query to refetch updated data + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY_PREFIX, address, chainId], + }) + }, + onError: error => { + console.error('Failed to request tokens:', error) + }, + }) +} + +// ============================================================================ +// Query Hook +// ============================================================================ + +/** + * Query hook to fetch faucet data for the connected account + * + * Fetches: + * - Daily token request limit + * - Number of requests made by the account today + * - Reset time for the account's daily limit + * + * @param options - Query configuration options + * @returns Query result with faucet state data + * + * @example + * ```tsx + * function FaucetInfo() { + * const { data, isLoading } = useFaucetQuery() + * + * if (isLoading) return + * if (!data) return null + * + * return ( + *
+ *

Requests remaining: {data.remainingRequests.toString()}

+ *

Can request: {data.canRequest ? 'Yes' : 'No'}

+ *
+ * ) + * } + * ``` + */ +export function useFaucetQuery( + options?: UseFaucetQueryOptions +): UseQueryResult { + const config = useConfig() + const { address } = useAccount() + const chainId = useChainId() + + return useQuery({ + queryKey: [QUERY_KEY_PREFIX, address, chainId] as const, + queryFn: async (): Promise => { + if (!address) { + throw new Error('Wallet not connected') + } + + const results = await readContracts(config, { + contracts: [ + { + chainId, + address: FAUCET.address, + abi: FAUCET.abi, + functionName: 'DAILY_LIMIT', + }, + { + chainId, + address: FAUCET.address, + abi: FAUCET.abi, + functionName: 'accountDailyRequests', + args: [address], + }, + { + chainId, + address: FAUCET.address, + abi: FAUCET.abi, + functionName: 'accountResetTime', + args: [address], + }, + ], + }) + + // Extract results with proper error handling + const [dailyLimitResult, requestsResult, resetTimeResult] = results + + if (dailyLimitResult.status === 'failure') { + throw new Error('Failed to fetch daily limit') + } + if (requestsResult.status === 'failure') { + throw new Error('Failed to fetch account requests') + } + if (resetTimeResult.status === 'failure') { + throw new Error('Failed to fetch reset time') + } + + // Type-safe extraction of results + const dailyLimit = dailyLimitResult.result as bigint + const accountDailyRequests = requestsResult.result as bigint + const accountResetTime = resetTimeResult.result as bigint + + // Calculate derived state + const remainingRequests = + dailyLimit > accountDailyRequests + ? dailyLimit - accountDailyRequests + : 0n + + const now = Math.floor(Date.now() / 1000) + + // const remainingRequests = dailyLimit > accountDailyRequests + const remainingAmount = + accountResetTime <= now ? dailyLimit : dailyLimit - accountDailyRequests + const actualUsedToday = + accountResetTime <= now ? 0n : accountDailyRequests + const hasUsedFaucet = actualUsedToday > 0n + const canClaim = remainingAmount > 0n + + return { + dailyLimit, + accountDailyRequests, + accountResetTime, + hasUsedFaucet, + actualUsedToday, + remainingAmount, + remainingRequests, + canClaim, + } + }, + enabled: options?.enabled ?? !!address, + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, + }) +} diff --git a/apps/hub/src/app/_hooks/useLockVault.ts b/apps/hub/src/app/_hooks/useLockVault.ts new file mode 100644 index 000000000..bf2a346d6 --- /dev/null +++ b/apps/hub/src/app/_hooks/useLockVault.ts @@ -0,0 +1,209 @@ +import { useToast } from '@status-im/components' +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { type Address, type Hash } from 'viem' +import { useAccount, useConfig, useReadContract, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { vaultAbi } from '~constants/contracts' +import { statusNetworkTestnet } from '~constants/index' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useVaultStateContext } from '~hooks/useVaultStateContext' +import { shortenAddress } from '~utils/address' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Parameters for locking a vault + */ +export interface LockVaultParams { + /** + * Increased lock duration in seconds + * The smart contract's _calculateLock handles all calculations: + * - New lock end = max(currentLockEnd, now) + increasedLockSeconds + * - Delta MP calculation + * - Validation (min/max periods, MP overflow) + */ + lockPeriodInSeconds: bigint +} + +/** + * Return type for the useLockVault hook + */ +export type UseLockVaultReturn = UseMutationResult< + Hash, + Error, + LockVaultParams, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY = 'lock-vault' as const + +const TRANSACTION_CONFIG = { + CONFIRMATION_BLOCKS: 1, +} as const + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Generates a success toast message based on the lock operation performed + * + * @param vaultAddress - The vault address that was locked/extended + * @param wasAlreadyLocked - True if extending an existing lock, false if creating a new lock + * @returns Formatted success message for the toast notification + */ +const formatLockSuccessMessage = ( + vaultAddress: Address, + wasAlreadyLocked: boolean +): string => { + if (wasAlreadyLocked) { + return 'Lock time extended successfully' + } + + return `Vault ${shortenAddress(vaultAddress)} has been locked` +} + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to lock or extend a staking vault's lock period + * + * **Locking Process:** + * Locks a vault for a specified duration to increase the boost multiplier. + * The longer the lock period, the higher the multiplier (max 9x). + * + * **How it works:** + * - Always pass the increased lock duration in seconds + * - The smart contract's `_calculateLock` function handles all calculations internally: + * - New lock end = max(currentLockEnd, now) + increasedLockSeconds + * - Delta MP = _bonusMP(balance, increasedLockSeconds) + * - Validation (min/max periods, MP overflow checks) + * + * **Process Flow:** + * 1. Validates wallet connection and lock period + * 2. Sends START_LOCK event to state machine + * 3. Calls `Vault.lock(increasedLockSeconds)` - contract handles the rest + * 4. Waits for transaction confirmation + * 5. Updates state machine and refetches vaults + * 6. Shows success toast notification + * + * @returns Mutation result with mutate function to trigger vault lock + * + * @throws {Error} When wallet is not connected + * @throws {Error} When lock period is invalid (zero or negative) + * @throws {Error} When transaction is reverted + * + * @example + * Locking or extending a vault for 30 days + * ```tsx + * function LockButton({ vaultAddress }: Props) { + * const { mutate: lockVault, isPending } = useLockVault(vaultAddress) + * + * const handleLock = () => { + * const thirtyDaysInSeconds = BigInt(30 * 24 * 60 * 60) + * lockVault({ + * lockPeriodInSeconds: thirtyDaysInSeconds, + * }) + * } + * + * return ( + * + * ) + * } + * ``` + */ +export function useLockVault(vaultAddress: Address): UseLockVaultReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + const { refetch: refetchStakingVaults } = useStakingVaults() + const toast = useToast() + + // Read current lockUntil to determine if vault is already locked + const { data: currentLockUntil } = useReadContract({ + abi: vaultAbi, + address: vaultAddress, + functionName: 'lockUntil', + }) as { data: bigint | undefined } + + return useMutation({ + mutationKey: [MUTATION_KEY, vaultAddress, address], + mutationFn: async ({ + lockPeriodInSeconds, + }: LockVaultParams): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Validate lock period + if (lockPeriodInSeconds <= 0n) { + throw new Error('Lock period must be greater than 0') + } + + // Notify state machine of lock start + sendVaultEvent({ type: 'START_LOCK' }) + + try { + // Call Vault.lock with the increased lock duration in seconds + // The smart contract's _calculateLock handles all the math + const hash = await writeContractAsync({ + chain: statusNetworkTestnet, + account: address, + address: vaultAddress, + abi: vaultAbi, + functionName: 'lock', + args: [lockPeriodInSeconds], + }) + + // Transaction submitted, notify state machine + sendVaultEvent({ type: 'SIGN' }) + + // Wait for transaction confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: TRANSACTION_CONFIG.CONFIRMATION_BLOCKS, + }) + + // Check for transaction revert + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + + // Check if vault was already locked before this transaction + const wasAlreadyLocked = currentLockUntil && currentLockUntil > 0n + + // Show success toast + toast.positive( + formatLockSuccessMessage(vaultAddress, !!wasAlreadyLocked) + ) + + // Reset state machine and refetch vaults + resetVault() + await refetchStakingVaults() + + return hash + } catch (error) { + console.error('Failed to lock vault:', error) + // Transaction failed or user rejected + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useMultiplierPoints.ts b/apps/hub/src/app/_hooks/useMultiplierPoints.ts new file mode 100644 index 000000000..c8c7acd07 --- /dev/null +++ b/apps/hub/src/app/_hooks/useMultiplierPoints.ts @@ -0,0 +1,217 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { type Address } from 'viem' +import { useAccount, useChainId, useConfig } from 'wagmi' +import { readContract } from 'wagmi/actions' + +import { STAKING_MANAGER } from '~constants/index' +import { type StakingVault, useStakingVaults } from '~hooks/useStakingVaults' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Result data for multiplier points balance query + */ +export interface MultiplierPointsData { + /** Map of vault addresses to their current MP balances */ + vaultBalances: Record + /** Total uncompounded MP across all vaults */ + totalUncompounded: bigint + /** Total multiplier points redeemed by a user */ + totalMpRedeemed: bigint +} + +/** + * Return type for the useMultiplierPointsBalance query hook + */ +export type UseMultiplierPointsBalanceReturn = + UseQueryResult + +// ============================================================================ +// Constants +// ============================================================================ + +const QUERY_KEYS = { + MULTIPLIER_POINTS: 'multiplier-points-balance', +} as const + +const DEFAULT_MP_DATA: MultiplierPointsData = { + vaultBalances: {}, + totalUncompounded: 0n, + totalMpRedeemed: 0n, +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Fetches the multiplier points balance for a specific vault + */ +async function fetchVaultMpBalance( + config: ReturnType, + vaultAddress: Address +): Promise { + const result = (await readContract(config, { + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'mpBalanceOf', + args: [vaultAddress], + })) as bigint + + return result +} + +/** + * Calculates total uncompounded MP by comparing vault balances with staked amounts + */ +function calculateTotalUncompounded( + vaultBalances: Record, + vaults: StakingVault[] +): bigint { + return vaults.reduce((total, vault) => { + const currentBalance = vaultBalances[vault.address] ?? 0n + const stakedAmount = vault.data?.mpAccrued ?? 0n + const uncompounded = + currentBalance > stakedAmount ? currentBalance - stakedAmount : 0n + return total + uncompounded + }, 0n) +} + +/** + * Fetches the total multiplier points redeemed by a user + */ +async function fetchMpBalanceOfAccount( + config: ReturnType, + chainId: number, + address: Address +): Promise { + try { + const result = (await readContract(config, { + chainId, + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'mpBalanceOfAccount', + args: [address], + })) as bigint + + return result + } catch (error) { + console.error( + `Failed to fetch account MP redeemed for ${address}:`, + error instanceof Error ? error.message : String(error) + ) + return 0n + } +} + +// ============================================================================ +// Query Hook +// ============================================================================ + +/** + * Query hook to fetch multiplier points balances across all vaults + * + * **Data Retrieved:** + * - Individual vault MP balances + * - Total uncompounded MP (difference between current balance and staked amount) + * + * @returns Query result with MP balance data + * + * @example + * Basic usage + * ```tsx + * function MultiplierPointsDisplay() { + * const { data, isLoading, error } = useMultiplierPointsBalance() + * + * if (isLoading) return
Loading...
+ * if (error) return
Error: {error.message}
+ * + * return ( + *
+ *

Total Uncompounded: {data?.totalUncompounded.toString()}

+ *

Vaults: {Object.keys(data?.vaultBalances ?? {}).length}

+ *
+ * ) + * } + * ``` + * + * @example + * With formatted display + * ```tsx + * function CompoundInfo() { + * const { data } = useMultiplierPointsBalance() + * const hasUncompounded = (data?.totalUncompounded ?? 0n) > 0n + * + * return ( + *
+ * {hasUncompounded ? ( + *

{formatSNT(data.totalUncompounded)} points ready to compound

+ * ) : ( + *

No points ready to compound

+ * )} + *
+ * ) + * } + * ``` + */ +export function useMultiplierPointsBalance(): UseMultiplierPointsBalanceReturn { + const { address } = useAccount() + const config = useConfig() + const chainId = useChainId() + const { data: vaults } = useStakingVaults() + + return useQuery({ + queryKey: [QUERY_KEYS.MULTIPLIER_POINTS, address, vaults?.length], + queryFn: async (): Promise => { + if (!vaults || vaults.length === 0 || !address || !chainId) { + return DEFAULT_MP_DATA + } + + try { + const vaultAddresses = vaults.map(vault => vault.address) + + // Fetch all MP balances in parallel + const balances = await Promise.all( + vaultAddresses.map(vaultAddress => + fetchVaultMpBalance(config, vaultAddress) + ) + ) + + // Map addresses to balances + const vaultBalances = vaultAddresses.reduce( + (acc, vaultAddress, index) => { + acc[vaultAddress] = balances[index] + return acc + }, + {} as Record + ) + + const totalMpRedeemed = await fetchMpBalanceOfAccount( + config, + chainId, + address + ) + + // Calculate total uncompounded MPs + const totalUncompounded = calculateTotalUncompounded( + vaultBalances, + vaults + ) + + return { + vaultBalances, + totalUncompounded, + totalMpRedeemed, + } + } catch (error) { + console.error('Failed to fetch multiplier points balances:', error) + return DEFAULT_MP_DATA + } + }, + enabled: !!address && !!vaults && vaults.length > 0, + staleTime: 30_000, // Consider data fresh for 30 seconds + refetchInterval: 60_000, // Refetch every 60 seconds + }) +} diff --git a/apps/hub/src/app/_hooks/useSIWE.ts b/apps/hub/src/app/_hooks/useSIWE.ts new file mode 100644 index 000000000..6d32af77a --- /dev/null +++ b/apps/hub/src/app/_hooks/useSIWE.ts @@ -0,0 +1,50 @@ +import { useCallback } from 'react' + +import { SiweMessage } from 'siwe' +import { useAccount, useSignMessage } from 'wagmi' + +export function useSiwe() { + const { address, chainId } = useAccount() + const { signMessageAsync } = useSignMessage() + + const signIn = useCallback(async () => { + if (!address || !chainId) { + throw new Error('Wallet not connected') + } + + // Create SIWE message + const message = new SiweMessage({ + domain: window.location.host, + address, + statement: 'Sign in with Ethereum to the app.', + uri: window.location.origin, + version: '1', + chainId, + nonce: await generateNonce(), + }) + + const preparedMessage = message.prepareMessage() + + // Sign the message + const signature = await signMessageAsync({ + message: preparedMessage, + }) + + // TODO: Verify the signature (you would typically do this on the backend) + // For now, we just return the signature + return { + message: preparedMessage, + signature, + } + }, [address, chainId, signMessageAsync]) + + return { + signIn, + } +} + +// Helper function to generate a nonce +async function generateNonce(): Promise { + const nonce = Math.random().toString(36).substring(2, 15) + return nonce +} diff --git a/apps/hub/src/app/_hooks/useSliderConfig.ts b/apps/hub/src/app/_hooks/useSliderConfig.ts new file mode 100644 index 000000000..7ff08f2d9 --- /dev/null +++ b/apps/hub/src/app/_hooks/useSliderConfig.ts @@ -0,0 +1,134 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { useConfig } from 'wagmi' +import { readContract } from 'wagmi/actions' + +import { STAKING_MANAGER } from '~constants/index' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Slider configuration for lockup periods + */ +export interface SliderConfig { + /** Minimum lockup period in seconds */ + min: number + /** Maximum lockup period in seconds */ + max: number +} + +/** + * Return type for the useSliderConfig hook + */ +export type UseSliderConfigReturn = UseQueryResult + +// ============================================================================ +// Constants +// ============================================================================ + +const QUERY_KEY_PREFIX = 'slider-config' as const +const STALE_TIME = 5 * 60 * 1000 // 5 minutes +const CACHE_TIME = 10 * 60 * 1000 // 10 minutes + +// ============================================================================ +// Query Hook +// ============================================================================ + +/** + * Query hook to fetch slider configuration for vault lockup periods + * + * Retrieves MIN_LOCKUP_PERIOD and MAX_LOCKUP_PERIOD from the staking manager + * contract to configure the lockup period slider UI component. + * + * @returns Query result with slider configuration data + * + * @throws {Error} When contract read fails + * @throws {Error} When contract returns invalid values + * + * @example + * Basic usage + * ```tsx + * function LockupSlider() { + * const { data: config, isLoading } = useSliderConfig() + * + * if (isLoading) return
Loading...
+ * if (!config) return
Failed to load config
+ * + * return ( + * + * ) + * } + * ``` + * + * @example + * With error handling + * ```tsx + * function LockupConfig() { + * const { data: config, isLoading, isError, error } = useSliderConfig() + * + * if (isLoading) return + * if (isError) return + * + * return ( + *
+ *

Min: {config.min} seconds

+ *

Max: {config.max} seconds

+ *
+ * ) + * } + * ``` + */ +export function useSliderConfig(): UseSliderConfigReturn { + const config = useConfig() + + return useQuery({ + queryKey: [QUERY_KEY_PREFIX], + queryFn: async (): Promise => { + try { + // Fetch minimum lockup period + const minLockupPeriod = await readContract(config, { + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'MIN_LOCKUP_PERIOD', + }) + + // Fetch maximum lockup period + const maxLockupPeriod = await readContract(config, { + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'MAX_LOCKUP_PERIOD', + }) + + // Validate results + if ( + typeof minLockupPeriod !== 'bigint' || + typeof maxLockupPeriod !== 'bigint' + ) { + throw new Error('Invalid lockup period values returned from contract') + } + + // Ensure min is less than max + if (minLockupPeriod >= maxLockupPeriod) { + throw new Error( + 'Minimum lockup period must be less than maximum lockup period' + ) + } + + return { + min: Number(minLockupPeriod), + max: Number(maxLockupPeriod), + } + } catch (error) { + throw new Error( + `Failed to fetch slider configuration: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }, + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + }) +} diff --git a/apps/hub/src/app/_hooks/useStakingVaults.ts b/apps/hub/src/app/_hooks/useStakingVaults.ts new file mode 100644 index 000000000..933d25cfd --- /dev/null +++ b/apps/hub/src/app/_hooks/useStakingVaults.ts @@ -0,0 +1,265 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { type Address } from 'viem' +import { useAccount, useChainId, useConfig } from 'wagmi' +import { readContract, readContracts } from 'wagmi/actions' + +import { vaultAbi } from '~constants/contracts' +import { STAKING_MANAGER } from '~constants/index' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Vault data structure returned from the StakingManager contract + * Maps directly to the VaultData struct in the smart contract + */ +export interface StakingVaultData { + /** Total amount of tokens staked in the vault */ + stakedBalance: bigint + /** Current reward index for calculating accrued rewards */ + rewardIndex: bigint + /** Amount of multiplier points (MP) accrued over time */ + mpAccrued: bigint + /** Maximum multiplier points achievable based on stake and lock period */ + maxMP: bigint + /** Timestamp of the last MP update */ + lastMPUpdateTime: bigint + /** Timestamp when the vault unlocks (0 if not locked) */ + lockUntil: bigint + /** Total rewards accrued and available for claiming */ + rewardsAccrued: bigint +} + +/** + * Vault with its address and associated data + */ +export interface StakingVault { + /** The vault contract address */ + address: Address + /** Vault data from the contract, or null if fetch failed */ + data: StakingVaultData | null +} + +/** + * Configuration options for the useStakingVaults hook + */ +export interface UseStakingVaultsOptions { + /** Whether to enable the query. Defaults to true when wallet is connected */ + enabled?: boolean + /** Cache duration in milliseconds. Default: 10000ms (10 seconds) */ + staleTime?: number + /** Auto-refetch interval in milliseconds. Default: disabled */ + refetchInterval?: number +} + +/** + * Return type for the useStakingVaults hook + */ +export type UseStakingVaultsReturn = UseQueryResult + +// ============================================================================ +// Constants +// ============================================================================ + +const QUERY_KEY = 'staking-vaults' as const + +const CACHE_CONFIG = { + DEFAULT_STALE_TIME: 10_000, // 10 seconds +} as const + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Fetches vault addresses for a given account from the StakingManager contract + */ +async function fetchAccountVaultAddresses( + config: ReturnType, + chainId: number, + accountAddress: Address +): Promise { + const addresses = (await readContract(config, { + chainId, + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'getAccountVaults', + args: [accountAddress], + })) as Address[] + + return addresses +} + +/** + * Fetches vault data for a specific vault address with error handling + */ +async function fetchVaultData( + config: ReturnType, + chainId: number, + vaultAddress: Address +): Promise { + try { + const results = await readContracts(config, { + contracts: [ + { + chainId, + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'getVault', + args: [vaultAddress], + }, + { + chainId, + address: vaultAddress, + abi: vaultAbi, + functionName: 'lockUntil', + args: [], + }, + ], + }) + + // Check if both contract calls succeeded + const [vaultResult, lockUntilResult] = results + + if ( + vaultResult.status !== 'success' || + lockUntilResult.status !== 'success' + ) { + console.error( + `Failed to fetch vault data for ${vaultAddress}:`, + vaultResult.status !== 'success' + ? vaultResult.error + : lockUntilResult.error + ) + return null + } + + // Extract the actual data from successful results + const vaultData = vaultResult.result as Omit + const lockUntil = lockUntilResult.result as bigint + + return { + ...vaultData, + lockUntil, + } + } catch (error) { + // Log error for debugging but don't throw - allows partial results + console.error( + `Failed to fetch vault data for ${vaultAddress}:`, + error instanceof Error ? error.message : String(error) + ) + return null + } +} + +/** + * Enriches a vault address with its data + */ +async function enrichVaultWithData( + config: ReturnType, + chainId: number, + vaultAddress: Address +): Promise { + const data = await fetchVaultData(config, chainId, vaultAddress) + return { address: vaultAddress, data } +} + +// ============================================================================ +// Hook +// ============================================================================ + +/** + * Query hook to fetch all staking vaults owned by the connected account + * + * **Features:** + * - Fetches vault addresses from StakingManager contract + * - Enriches each address with detailed vault data in parallel + * - Graceful error handling - failed vaults return with `data: null` + * - Automatic refetching and caching via React Query + * - Network-aware caching (includes chainId in query key) + * + * **Performance:** + * - Parallel fetching of vault data using Promise.all + * - Configurable cache duration via staleTime option + * - Optional auto-refetch via refetchInterval + * + * @param options - Configuration options for query behavior + * @returns React Query result with vault data + * + * @example + * Basic usage + * ```tsx + * function VaultsList() { + * const { data: vaults, isLoading, error } = useStakingVaults() + * + * if (isLoading) return + * if (error) return + * + * return ( + *
+ * {vaults?.map((vault) => + * vault.data ? ( + * + * ) : ( + * + * ) + * )} + *
+ * ) + * } + * ``` + * + * @example + * With custom options + * ```tsx + * const { data: vaults, refetch } = useStakingVaults({ + * enabled: isConnected, + * staleTime: 30_000, // 30 seconds + * refetchInterval: 60_000, // 1 minute + * }) + * ``` + * + * @example + * Filtering vaults with data + * ```tsx + * const { data: vaults } = useStakingVaults() + * const validVaults = vaults?.filter((v) => v.data !== null) ?? [] + * ``` + */ +export function useStakingVaults( + options?: UseStakingVaultsOptions +): UseStakingVaultsReturn { + const { address } = useAccount() + const config = useConfig() + const chainId = useChainId() + + return useQuery({ + queryKey: [QUERY_KEY, address, chainId], + queryFn: async (): Promise => { + // Guard: Return empty array if no wallet is connected + if (!address) { + return [] + } + + // Step 1: Fetch all vault addresses for the account + const vaultAddresses = await fetchAccountVaultAddresses( + config, + chainId, + address + ) + + // Step 2: Fetch detailed data for each vault in parallel + const vaults = await Promise.all( + vaultAddresses.map(vaultAddress => + enrichVaultWithData(config, chainId, vaultAddress) + ) + ) + + return vaults + }, + enabled: options?.enabled ?? !!address, + staleTime: options?.staleTime ?? CACHE_CONFIG.DEFAULT_STALE_TIME, + refetchInterval: options?.refetchInterval, + }) +} diff --git a/apps/hub/src/app/_hooks/useVaultStateContext.tsx b/apps/hub/src/app/_hooks/useVaultStateContext.tsx new file mode 100644 index 000000000..4153dbcf4 --- /dev/null +++ b/apps/hub/src/app/_hooks/useVaultStateContext.tsx @@ -0,0 +1,39 @@ +'use client' + +import { createContext, useContext } from 'react' + +import { useVaultStateMachine } from '~hooks/useVaultStateMachine' + +import type { VaultEvent, VaultState } from '~hooks/useVaultStateMachine' + +type VaultStateContextType = { + state: VaultState + send: (event: VaultEvent) => void + reset: () => void +} + +const VaultStateContext = createContext(null) + +export const VaultStateProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const stateMachine = useVaultStateMachine() + + return ( + + {children} + + ) +} + +export const useVaultStateContext = () => { + const context = useContext(VaultStateContext) + if (!context) { + throw new Error( + 'useVaultStateContext must be used within VaultStateProvider' + ) + } + return context +} diff --git a/apps/hub/src/app/_hooks/useVaultStateMachine.ts b/apps/hub/src/app/_hooks/useVaultStateMachine.ts new file mode 100644 index 000000000..211dc97bc --- /dev/null +++ b/apps/hub/src/app/_hooks/useVaultStateMachine.ts @@ -0,0 +1,354 @@ +import { useCallback, useState } from 'react' + +import { match } from 'ts-pattern' + +// State definitions +export type VaultState = + | { type: 'idle' } + | { type: 'siwe'; step: 'initialize' | 'processing' | 'rejected' } + | { type: 'createVault'; step: 'initialize' | 'processing' | 'rejected' } + | { + type: 'increaseAllowance' + step: 'initialize' | 'processing' | 'rejected' + amount?: string + } + | { + type: 'staking' + step: 'initialize' | 'processing' | 'rejected' + amount?: string + } + | { + type: 'withdraw' + step: 'processing' | 'rejected' + amount?: string + } + | { + type: 'lock' + step: 'initialize' | 'processing' | 'rejected' + } + | { + type: 'compound' + step: 'initialize' | 'processing' | 'rejected' + amount?: string + } + | { type: 'success' } + +// Event definitions +export type VaultEvent = + | { type: 'START_SIWE' } + | { type: 'START_CREATE_VAULT' } + | { type: 'START_INCREASE_ALLOWANCE'; amount?: string } + | { type: 'SIGN' } + | { type: 'REJECT' } + | { type: 'PROCESS'; amount?: string } + | { type: 'COMPLETE'; amount?: string } + | { type: 'READY_TO_STAKE' } + | { type: 'START_STAKING'; amount?: string } + | { type: 'START_WITHDRAW'; amount?: string } + | { type: 'START_LOCK' } + | { type: 'START_COMPOUND'; amount?: string } + | { type: 'RESET' } + +// Transition function +function transition(state: VaultState, event: VaultEvent): VaultState { + return ( + match<[VaultState, VaultEvent], VaultState>([state, event]) + .with([{ type: 'idle' }, { type: 'START_SIWE' }], () => ({ + type: 'siwe', + step: 'initialize', + })) + .with([{ type: 'idle' }, { type: 'START_CREATE_VAULT' }], () => ({ + type: 'createVault', + step: 'initialize', + })) + .with( + [{ type: 'idle' }, { type: 'START_INCREASE_ALLOWANCE' }], + ([, event]) => ({ + type: 'increaseAllowance', + step: 'initialize', + amount: event.amount, + }) + ) + .with([{ type: 'idle' }, { type: 'START_STAKING' }], ([, event]) => ({ + type: 'staking', + step: 'initialize', + amount: event.amount, + })) + .with([{ type: 'idle' }, { type: 'START_COMPOUND' }], ([, event]) => ({ + type: 'compound', + step: 'initialize', + amount: event.amount, + })) + + // SIWE flow + .with([{ type: 'siwe', step: 'initialize' }, { type: 'SIGN' }], () => ({ + type: 'siwe', + step: 'processing', + })) + .with([{ type: 'siwe', step: 'initialize' }, { type: 'REJECT' }], () => ({ + type: 'siwe', + step: 'rejected', + })) + .with( + [{ type: 'siwe', step: 'processing' }, { type: 'COMPLETE' }], + () => ({ + type: 'success', + }) + ) + .with([{ type: 'siwe', step: 'processing' }, { type: 'REJECT' }], () => ({ + type: 'siwe', + step: 'rejected', + })) + .with([{ type: 'siwe', step: 'processing' }, { type: 'RESET' }], () => ({ + type: 'idle', + })) + .with([{ type: 'siwe', step: 'rejected' }, { type: 'RESET' }], () => ({ + type: 'idle', + })) + + // Create Vault flow + .with( + [{ type: 'createVault', step: 'initialize' }, { type: 'SIGN' }], + () => ({ + type: 'createVault', + step: 'processing', + }) + ) + .with( + [{ type: 'createVault', step: 'initialize' }, { type: 'REJECT' }], + () => ({ + type: 'createVault', + step: 'rejected', + }) + ) + .with( + [{ type: 'createVault', step: 'processing' }, { type: 'REJECT' }], + () => ({ + type: 'createVault', + step: 'rejected', + }) + ) + .with( + [{ type: 'createVault', step: 'processing' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + .with( + [{ type: 'createVault', step: 'rejected' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + + // Increase Allowance flow + .with( + [{ type: 'increaseAllowance', step: 'initialize' }, { type: 'SIGN' }], + ([state]) => ({ + type: 'increaseAllowance', + step: 'processing', + amount: state.amount, + }) + ) + .with( + [{ type: 'increaseAllowance', step: 'initialize' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'increaseAllowance', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [ + { type: 'increaseAllowance', step: 'processing' }, + { type: 'COMPLETE' }, + ], + ([state, event]) => ({ + type: 'staking', + step: 'initialize', + amount: event.amount || state.amount || '0', + }) + ) + .with( + [{ type: 'increaseAllowance', step: 'processing' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'increaseAllowance', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [ + { type: 'increaseAllowance', step: 'rejected' }, + { type: 'START_INCREASE_ALLOWANCE' }, + ], + ([state, event]) => ({ + type: 'increaseAllowance', + step: 'initialize', + amount: event.amount || state.amount, + }) + ) + .with( + [{ type: 'increaseAllowance', step: 'rejected' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + + // Staking flow + .with( + [{ type: 'staking', step: 'initialize' }, { type: 'SIGN' }], + ([state]) => ({ + type: 'staking', + step: 'processing', + amount: state.amount, + }) + ) + .with( + [{ type: 'staking', step: 'processing' }, { type: 'COMPLETE' }], + () => ({ type: 'success' }) + ) + .with( + [{ type: 'staking', step: 'processing' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'staking', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [{ type: 'staking', step: 'processing' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + .with( + [{ type: 'staking', step: 'rejected' }, { type: 'START_STAKING' }], + ([state, event]) => ({ + type: 'staking', + step: 'initialize', + amount: event.amount || state.amount, + }) + ) + .with([{ type: 'staking', step: 'rejected' }, { type: 'RESET' }], () => ({ + type: 'idle', + })) + + // Withdraw flow - goes directly to processing when started + .with([{ type: 'idle' }, { type: 'START_WITHDRAW' }], ([, event]) => ({ + type: 'withdraw', + step: 'processing', + amount: event.amount, + })) + .with( + [{ type: 'withdraw', step: 'processing' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + .with( + [{ type: 'withdraw', step: 'processing' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'withdraw', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [{ type: 'withdraw', step: 'rejected' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + + // Lock flow + .with([{ type: 'idle' }, { type: 'START_LOCK' }], () => ({ + type: 'lock', + step: 'initialize', + })) + .with([{ type: 'lock', step: 'initialize' }, { type: 'SIGN' }], () => ({ + type: 'lock', + step: 'processing', + })) + .with([{ type: 'lock', step: 'initialize' }, { type: 'REJECT' }], () => ({ + type: 'lock', + step: 'rejected', + })) + .with([{ type: 'lock', step: 'processing' }, { type: 'REJECT' }], () => ({ + type: 'lock', + step: 'rejected', + })) + .with([{ type: 'lock', step: 'processing' }, { type: 'RESET' }], () => ({ + type: 'idle', + })) + .with( + [{ type: 'lock', step: 'rejected' }, { type: 'START_LOCK' }], + () => ({ + type: 'lock', + step: 'initialize', + }) + ) + .with([{ type: 'lock', step: 'rejected' }, { type: 'RESET' }], () => ({ + type: 'idle', + })) + + // compound flow + .with( + [{ type: 'compound', step: 'initialize' }, { type: 'SIGN' }], + ([state]) => ({ + type: 'compound', + step: 'processing', + amount: state.amount, + }) + ) + .with( + [{ type: 'compound', step: 'initialize' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'compound', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [{ type: 'compound', step: 'processing' }, { type: 'COMPLETE' }], + () => ({ type: 'success' }) + ) + .with( + [{ type: 'compound', step: 'processing' }, { type: 'REJECT' }], + ([state]) => ({ + type: 'compound', + step: 'rejected', + amount: state.amount, + }) + ) + .with( + [{ type: 'compound', step: 'processing' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + .with( + [{ type: 'compound', step: 'rejected' }, { type: 'START_COMPOUND' }], + ([state, event]) => ({ + type: 'compound', + step: 'initialize', + amount: event.amount || state.amount, + }) + ) + .with( + [{ type: 'compound', step: 'rejected' }, { type: 'RESET' }], + () => ({ type: 'idle' }) + ) + + // Reset from success + .with([{ type: 'success' }, { type: 'RESET' }], () => ({ type: 'idle' })) + + // Fallback + .otherwise(() => state) + ) +} + +// Hook +export function useVaultStateMachine() { + const [state, setState] = useState({ type: 'idle' }) + + const send = useCallback((event: VaultEvent) => { + setState(currentState => transition(currentState, event)) + }, []) + + const reset = useCallback(() => { + send({ type: 'RESET' }) + }, [send]) + + return { + state, + send, + reset, + } +} diff --git a/apps/hub/src/app/_hooks/useVaultTokenStake.ts b/apps/hub/src/app/_hooks/useVaultTokenStake.ts new file mode 100644 index 000000000..654c14775 --- /dev/null +++ b/apps/hub/src/app/_hooks/useVaultTokenStake.ts @@ -0,0 +1,171 @@ +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { type Address, formatUnits } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { vaultAbi } from '~constants/contracts' +import { SNT_TOKEN, statusNetworkTestnet } from '~constants/index' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useVaultStateContext } from '~hooks/useVaultStateContext' + +import { useMultiplierPointsBalance } from './useMultiplierPoints' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Parameters for staking to a vault + */ +export interface StakeParams { + /** Amount to stake in wei */ + amountWei: bigint + /** Lock period in seconds (0 for no lock) */ + lockPeriod: bigint + /** Vault address */ + vaultAddress: Address +} + +/** + * Return type for the useVaultStake hook + */ +export type UseVaultStakeReturn = UseMutationResult< + void, + Error, + StakeParams, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY_PREFIX = 'vault-stake' as const +const CONFIRMATION_BLOCKS = 1 + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to stake tokens to a vault + * + * Stakes tokens to the StakingManager contract with an optional lock period. + * Manages the state machine transitions for the staking process. + * + * @returns Mutation result with mutate function to trigger staking + * + * @throws {Error} When wallet is not connected + * @throws {Error} When transaction is reverted + * + * @example + * Basic usage + * ```tsx + * function StakeButton({ amount, lockPeriod }: { amount: bigint, lockPeriod: bigint }) { + * const { mutate: stake, isPending } = useVaultStake() + * + * return ( + * + * ) + * } + * ``` + * + * @example + * With success/error handling + * ```tsx + * function StakeManager() { + * const { mutate: stake, isPending } = useVaultStake() + * + * const handleStake = (amount: bigint, lockPeriod: bigint) => { + * stake( + * { amountWei: amount, lockPeriod }, + * { + * onSuccess: (txHash) => { + * console.log('Staked successfully! Transaction:', txHash) + * }, + * onError: (error) => { + * console.error('Failed to stake:', error) + * }, + * } + * ) + * } + * + * return + * } + * ``` + */ +export function useVaultTokenStake(): UseVaultStakeReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + const { refetch: refetchStakingVaults } = useStakingVaults() + const { refetch: refetchMultiplierPoints } = useMultiplierPointsBalance() + + return useMutation({ + mutationKey: [MUTATION_KEY_PREFIX, address], + mutationFn: async ({ + amountWei, + lockPeriod, + vaultAddress, + }: StakeParams): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Validate amount + if (amountWei <= 0n) { + throw new Error('Amount must be greater than 0') + } + + // Send state machine event to start staking + sendVaultEvent({ + type: 'START_STAKING', + amount: formatUnits(amountWei, SNT_TOKEN.decimals), + }) + + try { + // Execute staking transaction + const hash = await writeContractAsync({ + chain: statusNetworkTestnet, + account: address, + address: vaultAddress, + abi: vaultAbi, + functionName: 'stake', + args: [amountWei, lockPeriod], + }) + + // Transaction submitted successfully, transition to processing + sendVaultEvent({ type: 'SIGN' }) + + // Wait for transaction confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: CONFIRMATION_BLOCKS, + }) + + // Check if transaction was reverted + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + + // Transaction successful, refetch data and close dialog + await Promise.all([refetchStakingVaults(), refetchMultiplierPoints()]) + resetVault() + } catch (error) { + // Transaction failed or user rejected + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useVaultWithdraw.ts b/apps/hub/src/app/_hooks/useVaultWithdraw.ts new file mode 100644 index 000000000..4a6d9f117 --- /dev/null +++ b/apps/hub/src/app/_hooks/useVaultWithdraw.ts @@ -0,0 +1,160 @@ +import { useMutation, type UseMutationResult } from '@tanstack/react-query' +import { type Address } from 'viem' +import { useAccount, useConfig, useWriteContract } from 'wagmi' +import { waitForTransactionReceipt } from 'wagmi/actions' + +import { vaultAbi } from '~constants/contracts' +import { SNT_TOKEN, statusNetworkTestnet } from '~constants/index' +import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useVaultStateContext } from '~hooks/useVaultStateContext' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Parameters for withdrawing from a vault + */ +export interface WithdrawParams { + /** Amount to withdraw in wei */ + amountWei: bigint + /** Vault address */ + vaultAddress: Address + /** Optional callback called immediately after user signs transaction */ + onSigned?: () => void +} + +/** + * Return type for the useVaultWithdraw hook + */ +export type UseVaultWithdrawReturn = UseMutationResult< + void, + Error, + WithdrawParams, + unknown +> + +// ============================================================================ +// Constants +// ============================================================================ + +const MUTATION_KEY_PREFIX = 'vault-withdraw' as const +const CONFIRMATION_BLOCKS = 1 + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +/** + * Mutation hook to withdraw tokens from a vault + * + * Withdraws (unstakes) tokens from a vault contract. + * Manages the state machine transitions for the withdrawal process. + * + * **Process Flow:** + * 1. Validates wallet connection and amount + * 2. Calls vault.withdraw() and waits for user to sign + * 3. After signing, sends START_WITHDRAW event → Goes directly to processing state + * 4. Calls onSigned callback (typically to close modal) + * 5. Waits for transaction confirmation + * 6. On success: Refetches data and resets state machine + * 7. On error: Sends REJECT event → Shows rejected state + * + * @returns Mutation result with mutate function to trigger withdrawal + * + * @throws {Error} When wallet is not connected + * @throws {Error} When amount is invalid + * @throws {Error} When transaction is reverted + * + * @example + * Basic usage with modal closing after sign + * ```tsx + * function WithdrawModal({ amount, vaultAddress, onClose }: Props) { + * const { mutate: withdraw } = useVaultWithdraw() + * + * const handleWithdraw = () => { + * withdraw({ + * amountWei: amount, + * vaultAddress, + * onSigned: () => { + * // Close modal after user signs in wallet + * onClose() + * } + * }) + * } + * + * return + * } + * ``` + */ +export function useVaultWithdraw(): UseVaultWithdrawReturn { + const { address } = useAccount() + const { writeContractAsync } = useWriteContract() + const config = useConfig() + const { send: sendVaultEvent, reset: resetVault } = useVaultStateContext() + const { refetch: refetchStakingVaults } = useStakingVaults() + const { refetch: refetchMultiplierPoints } = useMultiplierPointsBalance() + + return useMutation({ + mutationKey: [MUTATION_KEY_PREFIX, address], + mutationFn: async ({ + amountWei, + vaultAddress, + onSigned, + }: WithdrawParams): Promise => { + // Validate wallet connection + if (!address) { + throw new Error( + 'Wallet not connected. Please connect your wallet first.' + ) + } + + // Validate amount + if (amountWei <= 0n) { + throw new Error('Amount must be greater than 0') + } + + try { + // Execute withdrawal transaction + const hash = await writeContractAsync({ + chain: statusNetworkTestnet, + account: address, + address: vaultAddress, + abi: vaultAbi, + functionName: 'withdraw', + args: [SNT_TOKEN.address, amountWei], + }) + + // Transaction submitted successfully, send START_WITHDRAW event to show processing state + sendVaultEvent({ + type: 'START_WITHDRAW', + amount: formatUnits(amountWei, SNT_TOKEN.decimals), + }) + + // Call onSigned callback to close the modal + onSigned?.() + + // Wait for transaction confirmation + const { status } = await waitForTransactionReceipt(config, { + hash, + confirmations: CONFIRMATION_BLOCKS, + }) + + // Check if transaction was reverted + if (status === 'reverted') { + sendVaultEvent({ type: 'REJECT' }) + throw new Error('Transaction was reverted') + } + + // Transaction successful, refetch data and close dialog + await Promise.all([refetchStakingVaults(), refetchMultiplierPoints()]) + resetVault() + } catch (error) { + // Transaction failed or user rejected + sendVaultEvent({ type: 'REJECT' }) + throw error + } + }, + }) +} diff --git a/apps/hub/src/app/_hooks/useWeightedBoost.ts b/apps/hub/src/app/_hooks/useWeightedBoost.ts new file mode 100644 index 000000000..424bf5ec5 --- /dev/null +++ b/apps/hub/src/app/_hooks/useWeightedBoost.ts @@ -0,0 +1,174 @@ +import { useMemo } from 'react' + +import { formatUnits } from 'viem' + +import { SNT_TOKEN } from '~constants/index' +import { type StakingVault } from '~hooks/useStakingVaults' + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Weighted boost calculation result + */ +export interface WeightedBoost { + /** Weighted aggregate boost multiplier as a number */ + value: number + /** Formatted boost multiplier (e.g., "1.25") */ + formatted: string + /** Total staked amount across all vaults */ + totalStaked: number + /** Whether any vaults have stake */ + hasStake: boolean +} + +// ============================================================================ +// Constants +// ============================================================================ + +const BASE_BOOST = 1.0 +const DEFAULT_DECIMALS = 2 + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Converts bigint token amount to number using formatUnits + * + * @param value - The bigint value to convert + * @param decimals - Number of decimals for the token + * @returns Numeric representation + */ +function toTokenAmount(value: bigint, decimals: number): number { + return Number(formatUnits(value, decimals)) +} + +/** + * Calculates the boost multiplier for a single vault + * + * Boost formula: (MP / Staked) + 1 + * + * @param stakedBalance - Amount of tokens staked in the vault + * @param mpAccrued - Multiplier points accrued + * @param decimals - Token decimals + * @returns Boost multiplier for the vault + */ +function calculateVaultBoost( + stakedBalance: bigint, + mpAccrued: bigint, + decimals: number +): number { + const staked = toTokenAmount(stakedBalance, decimals) + const mp = toTokenAmount(mpAccrued, decimals) + + if (staked === 0) return BASE_BOOST + + return mp / staked + BASE_BOOST +} + +/** + * Calculates weighted aggregate boost across all vaults + * + * The weighted boost is calculated as: + * Sum(Boost_i * Staked_i) / Sum(Staked_i) + * + * Where: + * - Boost_i = (MP_i / Staked_i) + 1 + * - Staked_i = staked balance in vault i + * + * @param vaults - Array of vaults with their data + * @param decimals - Token decimals + * @returns Weighted boost calculation result + */ +function calculateWeightedBoost( + vaults: StakingVault[], + decimals: number +): WeightedBoost { + let totalWeightedBoost = 0 + let totalStaked = 0 + + // Calculate weighted sum across all vaults + for (const vault of vaults) { + // Skip vaults with no data or no stake + if (!vault.data || vault.data.stakedBalance === 0n) { + continue + } + + const vaultStaked = toTokenAmount(vault.data.stakedBalance, decimals) + const vaultBoost = calculateVaultBoost( + vault.data.stakedBalance, + vault.data.mpAccrued, + decimals + ) + + totalWeightedBoost += vaultBoost * vaultStaked + totalStaked += vaultStaked + } + + // Handle no stake case + if (totalStaked === 0) { + return { + value: BASE_BOOST, + formatted: BASE_BOOST.toFixed(DEFAULT_DECIMALS), + totalStaked: 0, + hasStake: false, + } + } + + // Calculate weighted average + const weightedAverage = totalWeightedBoost / totalStaked + + return { + value: weightedAverage, + formatted: `x${weightedAverage.toFixed(DEFAULT_DECIMALS)}`, + totalStaked, + hasStake: true, + } +} + +// ============================================================================ +// Hook +// ============================================================================ + +/** + * Hook to calculate weighted aggregate boost across all user vaults + * + * The weighted boost rewards long-term staking by calculating a weighted + * average of individual vault boosts based on their staked amounts. + * + * @param vaults - Array of user vaults with their data + * @returns Weighted boost calculation result + * + * @example + * ```tsx + * function BoostDisplay() { + * const { data: vaults } = useStakingVaults() + * const boost = useWeightedBoost(vaults) + * + * return ( + *
+ *

Weighted Boost: x{boost.formatted}

+ *

Total Staked: {boost.totalStaked} SNT

+ *
+ * ) + * } + * ``` + */ +export function useWeightedBoost( + vaults: StakingVault[] | undefined +): WeightedBoost { + return useMemo(() => { + if (!vaults || vaults.length === 0) { + return { + value: BASE_BOOST, + formatted: BASE_BOOST.toFixed(DEFAULT_DECIMALS), + totalStaked: 0, + hasStake: false, + } + } + + return calculateWeightedBoost(vaults, SNT_TOKEN.decimals) + }, [vaults]) +} diff --git a/apps/hub/src/app/layout.tsx b/apps/hub/src/app/layout.tsx index 1064253d0..290319ef3 100644 --- a/apps/hub/src/app/layout.tsx +++ b/apps/hub/src/app/layout.tsx @@ -2,6 +2,8 @@ import './globals.css' import { Inter } from 'next/font/google' +import { Providers } from './providers' + import type { Metadata } from 'next' const inter = Inter({ @@ -22,7 +24,9 @@ export default function RootLayout({ }) { return ( - {children} + + {children} + ) } diff --git a/apps/hub/src/app/providers.tsx b/apps/hub/src/app/providers.tsx new file mode 100644 index 000000000..620268fc7 --- /dev/null +++ b/apps/hub/src/app/providers.tsx @@ -0,0 +1,57 @@ +'use client' + +import { ToastContainer } from '@status-im/components' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ConnectKitProvider } from 'connectkit' +import { WagmiProvider } from 'wagmi' + +import { ActionStatusDialog } from '~components/stake/action-status-dialog' +import { useActionStatusContent } from '~components/stake/hooks/use-action-status-content' +import { statusNetworkTestnet } from '~constants/index' +import { + useVaultStateContext, + VaultStateProvider, +} from '~hooks/useVaultStateContext' + +import { defineWagmiConfig } from '../wagmi' + +const config = defineWagmiConfig({ + chains: [statusNetworkTestnet], + ssr: false, +}) + +const queryClient = new QueryClient() + +const ProvidersContent = ({ children }: { children: React.ReactNode }) => { + const { state: vaultState, reset: resetVault } = useVaultStateContext() + const dialogContent = useActionStatusContent(vaultState) + + return ( + <> + {children} + + {dialogContent && ( + + )} + + ) +} + +export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + + + + + {children} + + + + + + ) +} diff --git a/apps/hub/src/app/stake/page.tsx b/apps/hub/src/app/stake/page.tsx index 51cfa71fe..078320c6c 100644 --- a/apps/hub/src/app/stake/page.tsx +++ b/apps/hub/src/app/stake/page.tsx @@ -1,105 +1,515 @@ 'use client' +import { useMemo, useState } from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { Tooltip } from '@status-im/components' +import { + DropdownIcon, + ExternalIcon, + InfoIcon, + PlaceholderIcon, +} from '@status-im/icons/20' +import { Button, ButtonLink } from '@status-im/status-network/components' +import { ConnectKitButton } from 'connectkit' +import Image from 'next/image' +import { useForm, useWatch } from 'react-hook-form' +import { match } from 'ts-pattern' +import { formatUnits, parseUnits } from 'viem' +import { useAccount, useBalance, useConfig, useReadContract } from 'wagmi' +import { readContract } from 'wagmi/actions' +import { z } from 'zod' + import { HubLayout } from '~components/hub-layout' +import { LaunchIcon, SNTIcon } from '~components/icons' +import { PromoModal } from '~components/stake/promo-modal' +import { VaultSelect } from '~components/vault-select' +import { VaultsTable } from '~components/vaults/vaults-table' +import { + SNT_TOKEN, + STAKING_MANAGER, + statusNetworkTestnet, +} from '~constants/index' +import { useApproveToken } from '~hooks/useApproveToken' +import { useCompoundMultiplierPoints } from '~hooks/useCompoundMultiplierPoints' +import { useCreateVault } from '~hooks/useCreateVault' +import { useExchangeRate } from '~hooks/useExchangeRate' +import { useFaucetMutation, useFaucetQuery } from '~hooks/useFaucet' +import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints' +import { useStakingVaults } from '~hooks/useStakingVaults' +import { useVaultStateContext } from '~hooks/useVaultStateContext' +import { useVaultTokenStake } from '~hooks/useVaultTokenStake' +import { useWeightedBoost } from '~hooks/useWeightedBoost' +import { formatCurrency, formatSNT } from '~utils/currency' + +import type { Address } from 'viem' + +const createStakeFormSchema = () => { + return z.object({ + amount: z.string(), + vault: z.string(), + }) +} + +type FormValues = z.infer> + +type ConnectionStatus = 'uninstalled' | 'disconnected' | 'connected' export default function StakePage() { + const [isPromoModalOpen, setIsPromoModalOpen] = useState(false) + + const { isConnected, address } = useAccount() + const config = useConfig() + const { mutate: compoundMultiplierPoints } = useCompoundMultiplierPoints() + const { data: multiplierPointsData } = useMultiplierPointsBalance() + + const { mutate: claimTokens, isPending: isClaimingTokens } = + useFaucetMutation() + const { data: faucetData } = useFaucetQuery() + const { data: vaults, refetch: refetchStakingVaults } = useStakingVaults() + const weightedBoost = useWeightedBoost(vaults) + const { data: exchangeRate } = useExchangeRate() + + const form = useForm({ + resolver: zodResolver(createStakeFormSchema()), + mode: 'onChange', + defaultValues: { + amount: '', + vault: '', + }, + }) + + const { data: balance } = useBalance({ + chainId: statusNetworkTestnet.id, + scopeKey: 'balance', + address, + token: SNT_TOKEN.address, + query: { + enabled: isConnected, + }, + }) + + const { data: totalStaked } = useReadContract({ + address: STAKING_MANAGER.address, + abi: STAKING_MANAGER.abi, + functionName: 'totalStaked', + }) as { data: bigint } + + const { mutate: createVault } = useCreateVault() + const { mutate: approveToken } = useApproveToken() + const { mutate: stakeVault } = useVaultTokenStake() + + // State machine for vault operations + const { send: sendVaultEvent } = useVaultStateContext() + + const status: ConnectionStatus = useMemo(() => { + if (isConnected) return 'connected' + return isPromoModalOpen ? 'uninstalled' : 'disconnected' + }, [isConnected, isPromoModalOpen]) + + // Watch the amount field for changes + const amountValue = useWatch({ + control: form.control, + name: 'amount', + defaultValue: '', + }) + + const amountInUSD = useMemo(() => { + const amountInputNumber = parseFloat(amountValue || '0') + if (exchangeRate && !isNaN(amountInputNumber)) { + return amountInputNumber * exchangeRate.price + } + return 0 + }, [amountValue, exchangeRate]) + + const { + isDisabled: isDisabledMultiplierPoints, + message: messageMultiplierPoints, + } = useMemo(() => { + const totalUncompounded = multiplierPointsData?.totalUncompounded ?? 0n + const hasUncompoundedPoints = totalUncompounded >= 0n + const formattedAmount = formatSNT( + formatUnits(totalUncompounded, SNT_TOKEN.decimals), + { + decimals: SNT_TOKEN.decimals, + } + ) + + return { + isDisabled: !hasUncompoundedPoints || !isConnected, + message: hasUncompoundedPoints + ? `${formattedAmount} points are ready to compound` + : 'No points are ready to compound', + } + }, [multiplierPointsData, isConnected]) + + const hasReachedDailyLimit = useMemo(() => { + if (!faucetData) return false + + const currentTimestamp = BigInt(Math.floor(Date.now() / 1000)) + const isWithinResetWindow = faucetData.accountResetTime > currentTimestamp + const hasExceededLimit = + faucetData.accountDailyRequests >= faucetData.dailyLimit + + return hasExceededLimit && isWithinResetWindow + }, [faucetData]) + + const handleSubmit = async (data: FormValues) => { + if (!data.amount || !data.vault || !address) return + + try { + const amountWei = parseUnits(data.amount, SNT_TOKEN.decimals) + + // Check current allowance + const currentAllowance = (await readContract(config, { + address: SNT_TOKEN.address, + abi: SNT_TOKEN.abi, + functionName: 'allowance', + args: [address, data.vault as Address], + })) as bigint + + // Transition to increase allowance if not enough allowance + if (amountWei >= currentAllowance) { + approveToken( + { + amount: data.amount, + spenderAddress: data.vault as Address, + }, + { + onSuccess: () => { + // After approval completes, proceed to staking + stakeVault({ + amountWei, + lockPeriod: 0n, + vaultAddress: data.vault as Address, + }) + }, + onError: error => { + throw error + }, + } + ) + } else { + // Allowance already sufficient, go straight to staking + stakeVault({ + amountWei, + lockPeriod: 0n, + vaultAddress: data.vault as Address, + }) + } + } catch { + sendVaultEvent({ type: 'REJECT' }) + } finally { + form.reset() + refetchStakingVaults() + } + } + return ( -
- {/* Hero Section */} -
-

- Staking is good for karma -

-

- Stake SNT to increase your Karma and unlock gasless transactions +

+
+

Stake SNT, receive good Karma

+

+ Stake SNT to increase your Karma, unlock more gasless transactions + and increase your power over the network

-
- - {/* Main Content */} -
-
-
-
- {/* Amount to Stake */} -
-
) } + +const InfoTooltip = () => ( + + + The longer SNT is staked or locked in vaults, the higher this + multiplier goes. This rewards long term believers. The maximum + multiplier is x9. + + + + Learn more + + +
+ } + > + + +) diff --git a/apps/hub/src/utils/address.ts b/apps/hub/src/utils/address.ts new file mode 100644 index 000000000..d8d0330da --- /dev/null +++ b/apps/hub/src/utils/address.ts @@ -0,0 +1,3 @@ +export const shortenAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}` +} diff --git a/apps/hub/src/utils/currency.ts b/apps/hub/src/utils/currency.ts new file mode 100644 index 000000000..18a00036d --- /dev/null +++ b/apps/hub/src/utils/currency.ts @@ -0,0 +1,389 @@ +import { formatUnits, parseUnits } from 'viem' + +// ============================================================================ +// Types +// ============================================================================ + +export interface FormatTokenOptions { + /** + * Number of decimal places to display + * @default 2 + */ + decimals?: number + /** + * Minimum number of decimal places + * @default decimals value + */ + minimumFractionDigits?: number + /** + * Maximum number of decimal places + * @default decimals value + */ + maximumFractionDigits?: number + /** + * Whether to include the token symbol + * @default false + */ + includeSymbol?: boolean + /** + * Locale for number formatting + * @default 'en-US' + */ + locale?: string + /** + * Whether to use compact notation (K, M, B) + * @default false + */ + compact?: boolean + /** + * Whether to round down instead of rounding to nearest + * @default false + */ + roundDown?: boolean + /** + * Number of decimals for the token (used for bigint conversion) + * @default 18 + */ + tokenDecimals?: number +} + +// ============================================================================ +// Constants +// ============================================================================ + +const DEFAULT_LOCALE = 'en-US' +const DEFAULT_DISPLAY_DECIMALS = 2 +const DEFAULT_TOKEN_DECIMALS = 18 +const COMPACT_THRESHOLD = 1_000_000 + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Convert bigint to number using viem's formatUnits for precision + * + * @param value - The bigint value to convert + * @param decimals - Number of decimals for the token + * @returns Numeric representation of the token amount + */ +function bigIntToNumber( + value: bigint, + decimals: number = DEFAULT_TOKEN_DECIMALS +): number { + // Use viem's formatUnits for proper decimal handling + const formatted = formatUnits(value, decimals) + return Number(formatted) +} + +/** + * Converts various input types to a numeric value + * + * @param amount - The amount to convert (bigint, number, or string) + * @param tokenDecimals - Number of decimals for bigint conversion + * @returns Numeric representation of the amount + */ +function toNumericAmount( + amount: number | bigint | string, + tokenDecimals: number +): number { + if (typeof amount === 'bigint') { + return bigIntToNumber(amount, tokenDecimals) + } + + if (typeof amount === 'string') { + return Number(amount) + } + + return amount +} + +/** + * Applies rounding down to a numeric value + * + * @param value - The value to round + * @param decimals - Number of decimal places to preserve + * @returns Rounded down value + */ +function roundDownToDecimals(value: number, decimals: number): number { + const factor = 10 ** decimals + return Math.floor(value * factor) / factor +} + +// ============================================================================ +// Core Formatting Functions +// ============================================================================ + +/** + * Format a token amount with proper decimal handling using viem utilities + * + * @param amount - Token amount (supports bigint, number, or string) + * @param token - Token symbol (e.g., 'SNT', 'ETH') + * @param options - Formatting options + * @returns Formatted token amount string + * + * @example + * ```ts + * formatTokenAmount(1234567890000000000n, 'SNT', { tokenDecimals: 18 }) + * // => "1.23" + * + * formatTokenAmount(1500, 'SNT', { includeSymbol: true, compact: true }) + * // => "1.5K SNT" + * ``` + */ +export function formatTokenAmount( + amount: number | bigint | string, + token: string, + options: FormatTokenOptions = {} +): string { + const { + decimals = DEFAULT_DISPLAY_DECIMALS, + minimumFractionDigits = decimals, + maximumFractionDigits = decimals, + includeSymbol = false, + locale = DEFAULT_LOCALE, + compact = false, + roundDown = false, + tokenDecimals = DEFAULT_TOKEN_DECIMALS, + } = options + + // Convert to numeric value + let numericAmount = toNumericAmount(amount, tokenDecimals) + + // Apply rounding down if requested + if (roundDown) { + numericAmount = roundDownToDecimals(numericAmount, maximumFractionDigits) + } + + // Format using Intl.NumberFormat + const formatter = new Intl.NumberFormat(locale, { + minimumFractionDigits, + maximumFractionDigits, + notation: compact ? 'compact' : 'standard', + compactDisplay: compact ? 'short' : undefined, + }) + + const formatted = formatter.format(numericAmount) + + return includeSymbol ? `${formatted} ${token}` : formatted +} + +// ============================================================================ +// Token-Specific Formatters +// ============================================================================ + +/** + * Format SNT (Status Network Token) amount + * + * @param amount - Token amount in wei (bigint) or display units (number/string) + * @param options - Formatting options + * @returns Formatted SNT amount + * + * @example + * ```ts + * formatSNT(1234567890000000000n) // => "1.23" + * formatSNT(100, { includeSymbol: true }) // => "100.00 SNT" + * ``` + */ +export function formatSNT( + amount: number | bigint | string, + options: Omit = {} +): string { + return formatTokenAmount(amount, 'SNT', { + ...options, + tokenDecimals: DEFAULT_TOKEN_DECIMALS, // SNT has 18 decimals + }) +} + +/** + * Format KARMA token amount + * + * @param amount - Token amount + * @param options - Formatting options + * @returns Formatted KARMA amount + */ +export function formatKarma( + amount: number | bigint | string, + options: Omit = {} +): string { + return formatTokenAmount(amount, 'KARMA', { + ...options, + tokenDecimals: 0, // KARMA values are already in display units + }) +} + +/** + * Format ETH (Ether) amount + * + * @param amount - ETH amount in wei (bigint) or display units (number/string) + * @param options - Formatting options + * @returns Formatted ETH amount + * + * @example + * ```ts + * formatETH(1000000000000000000n) // => "1.0000" + * formatETH(0.123456789, { decimals: 6 }) // => "0.123457" + * ``` + */ +export function formatETH( + amount: number | bigint | string, + options: Omit = {} +): string { + return formatTokenAmount(amount, 'ETH', { + decimals: 4, // Default to 4 decimals for ETH + ...options, + tokenDecimals: DEFAULT_TOKEN_DECIMALS, // ETH has 18 decimals + }) +} + +/** + * Format stablecoin amount (USDC, USDT, DAI) + * + * @param amount - Stablecoin amount in smallest unit (bigint) or display units (number/string) + * @param token - Stablecoin symbol + * @param options - Formatting options + * @returns Formatted stablecoin amount + * + * @example + * ```ts + * formatStablecoin(1000000n, 'USDC') // => "1.00" (USDC has 6 decimals) + * formatStablecoin(1000000000000000000n, 'DAI') // => "1.00" (DAI has 18 decimals) + * ``` + */ +export function formatStablecoin( + amount: number | bigint | string, + token: 'USDC' | 'USDT' | 'DAI', + options: Omit = {} +): string { + // USDC and USDT typically have 6 decimals, DAI has 18 + const decimals = token === 'DAI' ? DEFAULT_TOKEN_DECIMALS : 6 + + return formatTokenAmount(amount, token, { + ...options, + tokenDecimals: decimals, + }) +} + +/** + * Format a currency value for display in UI + * Uses Intl.NumberFormat with USD currency by default + * Automatically handles large numbers with compact notation + * + * @param amount - Currency amount + * @param options - Formatting options + * @returns Formatted currency string + * + * @example + * ```ts + * formatCurrency(1234567) // => "$1.23M" + * formatCurrency(999) // => "$999.00" + * formatCurrency(1500000n, { tokenDecimals: 18 }) // => "$0.00" + * formatCurrency(100, { currency: 'EUR' }) // => "€100.00" + * ``` + */ +export function formatCurrency( + amount: number | bigint | string, + options: FormatTokenOptions & { currency?: string } = {} +): string { + const { + currency = 'USD', + tokenDecimals = 0, // Default to 0 for currency values (already in display units) + locale = DEFAULT_LOCALE, + decimals = DEFAULT_DISPLAY_DECIMALS, + ...rest + } = options + + const numericAmount = toNumericAmount(amount, tokenDecimals) + + // Auto-enable compact notation for large numbers (>= 1M) + const shouldCompact = + rest.compact !== false && Math.abs(numericAmount) >= COMPACT_THRESHOLD + + const formatter = new Intl.NumberFormat(locale, { + style: 'currency', + currency, + minimumFractionDigits: rest.minimumFractionDigits ?? decimals, + maximumFractionDigits: rest.maximumFractionDigits ?? decimals, + notation: shouldCompact ? 'compact' : 'standard', + compactDisplay: 'short', + }) + + return formatter.format(numericAmount) +} + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Compact notation multipliers + */ +const COMPACT_MULTIPLIERS: Record = { + K: 1_000, + M: 1_000_000, + B: 1_000_000_000, + T: 1_000_000_000_000, +} as const + +/** + * Parse a formatted token string back to a number + * + * Handles: + * - Comma-separated numbers (1,234.56) + * - Compact notation (1.5K, 2M, 3.5B) + * - Token symbols (100 SNT) + * + * @param formattedAmount - The formatted string to parse + * @param token - Optional token symbol to remove from the string + * @returns Numeric value + * + * @example + * ```ts + * parseTokenAmount('1,234.56') // => 1234.56 + * parseTokenAmount('1.5K') // => 1500 + * parseTokenAmount('100 SNT', 'SNT') // => 100 + * ``` + */ +export function parseTokenAmount( + formattedAmount: string, + token?: string +): number { + // Remove token symbol if present + let cleaned = formattedAmount + if (token) { + cleaned = cleaned.replace(token, '').trim() + } + + // Remove commas and spaces + cleaned = cleaned.replace(/[,\s]/g, '') + + // Handle compact notation (K, M, B, T) + const compactMatch = cleaned.match(/^([\d.]+)([KMBT])$/i) + if (compactMatch) { + const [, numStr, suffix] = compactMatch + const multiplier = COMPACT_MULTIPLIERS[suffix.toUpperCase()] ?? 1 + return Number(numStr) * multiplier + } + + return Number(cleaned) +} + +/** + * Parse a user input amount and convert to wei (bigint) using viem's parseUnits + * + * @param amount - User input amount (e.g., "1.5", "100") + * @param decimals - Number of decimals for the token + * @returns Amount in wei as bigint + * + * @example + * ```ts + * parseToWei('1.5', 18) // => 1500000000000000000n + * parseToWei('100', 6) // => 100000000n (for USDC) + * ``` + */ +export function parseToWei( + amount: string, + decimals: number = DEFAULT_TOKEN_DECIMALS +): bigint { + return parseUnits(amount, decimals) +} diff --git a/apps/hub/src/utils/vault.ts b/apps/hub/src/utils/vault.ts new file mode 100644 index 000000000..f37b4fc5b --- /dev/null +++ b/apps/hub/src/utils/vault.ts @@ -0,0 +1,55 @@ +import { type Address, formatUnits } from 'viem' + +import { SNT_TOKEN } from '~constants/index' + +import type { StakingVault } from '~hooks/useStakingVaults' + +/** + * Calculates the boost multiplier for a vault + * + * @param vaults - The list of vaults + * @param vaultAddress - The address of the vault + * @returns The boost multiplier for the vault + */ +export function calculateVaultBoost( + vaults: StakingVault[], + vaultAddress: Address +): string { + const account = vaults.find(vault => vault.address === vaultAddress) + if (!account || account.data?.stakedBalance === 0n) return '1.00' + const vaultStaked = Number( + formatUnits(account.data?.stakedBalance || 0n, SNT_TOKEN.decimals) + ) + const vaultMp = Number( + formatUnits(account.data?.mpAccrued || 0n, SNT_TOKEN.decimals) + ) + // Add 1 to reflect the boost (base multiplier is 1x) + const boost = vaultMp / vaultStaked + 1 + return boost.toFixed(2) +} + +/** + * Checks if a vault is locked based on the lockUntil timestamp + * + * @param lockUntil - The lockUntil timestamp in seconds + * @returns True if the vault is locked, false otherwise + */ +export function isVaultLocked(lockUntil: bigint | undefined) { + if (!lockUntil) return false + const now = Math.floor(Date.now() / 1000) + const lockUntilTimestamp = Number(lockUntil) + return lockUntilTimestamp > now +} + +/** + * Calculates the number of days until a vault unlocks + * + * @param lockUntil - The lockUntil timestamp in seconds + * @returns The number of days until the vault unlocks, or null if the vault is not locked + */ +export function calculateDaysUntilUnlock(lockUntil: bigint | undefined) { + if (!lockUntil) return null + const now = Math.floor(Date.now() / 1000) + const lockUntilTimestamp = Number(lockUntil) + return Math.ceil((lockUntilTimestamp - now) / 86400) +} diff --git a/apps/hub/src/wagmi.ts b/apps/hub/src/wagmi.ts new file mode 100644 index 000000000..2ee534d06 --- /dev/null +++ b/apps/hub/src/wagmi.ts @@ -0,0 +1,96 @@ +import { createConfig, http } from 'wagmi' + +import type { Config, CreateConfigParameters, Transport } from 'wagmi' +import type { Chain } from 'wagmi/chains' + +// Todo: move this to shared packages + +/** + * Configuration options for Status Hub wagmi config + */ +export interface DefineWagmiConfigOptions { + /** + * Array of chains to support + */ + chains: [Chain, ...Chain[]] + + /** + * Enable server-side rendering support + * @default false + */ + ssr?: boolean + + /** + * Custom RPC URLs per chain (optional) + * Maps chain IDs to RPC URLs + */ + rpcUrls?: Record + + /** + * Batch JSON-RPC requests + * @default undefined + */ + batch?: CreateConfigParameters['batch'] + + /** + * Polling interval in milliseconds + * @default undefined + */ + pollingInterval?: number + + /** + * WalletConnect project ID + * @default undefined + */ + walletConnectProjectId?: string +} + +/** + * Creates a wagmi configuration for Status Hub with the specified chains and options + * + * @param options - Configuration options + * @returns Wagmi config instance + * + * @example + * ```ts + * import { mainnet, optimism } from 'wagmi/chains' + * + * const config = defineWagmiConfig({ + * chains: [mainnet, optimism], + * ssr: true, + * walletConnectProjectId: 'your-project-id', + * rpcUrls: { + * [mainnet.id]: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY' + * } + * }) + * ``` + */ +export const defineWagmiConfig = ( + options: DefineWagmiConfigOptions +): Config => { + const { chains, ssr = false, rpcUrls, batch, pollingInterval } = options + + const transports = chains.reduce( + (acc, chain) => { + const rpcUrl = rpcUrls?.[chain.id] + acc[chain.id] = http(rpcUrl, { + batch: batch ? true : undefined, + ...(pollingInterval && { pollingInterval }), + }) + return acc + }, + {} as Record + ) + + return createConfig({ + chains, + ssr, + transports, + batch, + }) +} + +/** + * Type helper to infer the config type from defineWagmiConfig + */ +export type InferWagmiConfig = ReturnType diff --git a/apps/hub/tsconfig.json b/apps/hub/tsconfig.json index 505a9f1b6..aac604240 100644 --- a/apps/hub/tsconfig.json +++ b/apps/hub/tsconfig.json @@ -30,6 +30,9 @@ "baseUrl": ".", "paths": { "~components/*": ["./src/app/_components/*"], + "~hooks/*": ["./src/app/_hooks/*"], + "~constants/*": ["./src/app/_constants/*"], + "~utils/*": ["./src/utils/*"], "~/*": ["./src/*"] } }, diff --git a/package.json b/package.json index 7fd9d7b7b..a75c5eb11 100644 --- a/package.json +++ b/package.json @@ -82,5 +82,10 @@ "oslo@1.0.1": "patches/oslo@1.0.1.patch", "@trpc/server@11.1.0": "patches/@trpc__server@11.1.0.patch" } + }, + "dependencies": { + "@tanstack/react-query": "^5.90.2", + "connectkit": "^1.9.0", + "wagmi": "^2.12.8" } } diff --git a/packages/components/src/button/button.tsx b/packages/components/src/button/button.tsx index b8d427983..1c6f4a196 100644 --- a/packages/components/src/button/button.tsx +++ b/packages/components/src/button/button.tsx @@ -28,7 +28,7 @@ type Props = { } ) -type ButtonProps = Prettify< +export type ButtonProps = Prettify< Omit, 'children'> & { href?: never } > diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx index db783b104..616ddf04b 100644 --- a/packages/components/src/button/index.tsx +++ b/packages/components/src/button/index.tsx @@ -1 +1,2 @@ +export type { ButtonProps } from './button' export { Button } from './button' diff --git a/packages/components/src/toast/toast.tsx b/packages/components/src/toast/toast.tsx index 2e44675fd..a22f03e14 100644 --- a/packages/components/src/toast/toast.tsx +++ b/packages/components/src/toast/toast.tsx @@ -46,9 +46,11 @@ const Toast = (props: Props, ref: React.Ref) => {
)} - - - + {onAction && ( + + + + )}
) } diff --git a/packages/status-network/src/components/button/index.tsx b/packages/status-network/src/components/button/index.tsx index 21a8de0df..0789c5565 100644 --- a/packages/status-network/src/components/button/index.tsx +++ b/packages/status-network/src/components/button/index.tsx @@ -3,7 +3,7 @@ import { forwardRef } from 'react' import { cva, cx } from 'cva' type Props = { - variant?: 'primary' | 'secondary' | 'white' + variant?: 'primary' | 'secondary' | 'white' | 'outline' | 'danger' backdropFilter?: boolean children?: React.ReactNode active?: boolean @@ -22,6 +22,9 @@ const buttonStyles = cva({ 'border-white-10 bg-white-5 text-white-100 hover:border-white-20 hover:bg-white-10', white: 'border-neutral-30 bg-white-100 text-dark-100 hover:border-neutral-40 hover:bg-white-80', + outline: + 'pressed:border-neutral-50 border border-neutral-30 text-neutral-100 hover:border-neutral-40 disabled:border-neutral-20', + danger: 'border-[transparent]', }, withIcon: { true: '', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b37dba14..9e71c27b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,16 @@ patchedDependencies: importers: .: + dependencies: + '@tanstack/react-query': + specifier: ^5.90.2 + version: 5.90.2(react@18.3.1) + connectkit: + specifier: ^1.9.0 + version: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + wagmi: + specifier: ^2.12.8 + version: 2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) devDependencies: '@changesets/cli': specifier: ^2.26.2 @@ -113,7 +123,7 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/next': specifier: 10.45.2 - version: 10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 10.45.2(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@trpc/server': specifier: 10.45.2 version: 10.45.2 @@ -251,6 +261,12 @@ importers: apps/hub: dependencies: + '@hookform/devtools': + specifier: ^4.3.1 + version: 4.4.0(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@hookform/resolvers': + specifier: ^3.1.1 + version: 3.10.0(react-hook-form@7.64.0(react@19.1.0)) '@status-im/colors': specifier: workspace:* version: link:../../packages/colors @@ -263,9 +279,12 @@ importers: '@status-im/status-network': specifier: workspace:* version: link:../../packages/status-network + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) + version: 1.5.0(next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) cva: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(typescript@5.8.3) @@ -274,7 +293,7 @@ importers: version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 15.1.6 - version: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + version: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) next-mdx-remote: specifier: ^5.0.0 version: 5.0.0(@types/react@19.1.0)(acorn@8.14.1)(react@19.1.0) @@ -284,12 +303,21 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.45.1 + version: 7.64.0(react@19.1.0) rehype-slug: specifier: ^6.0.0 version: 6.0.0 + siwe: + specifier: ^2.3.2 + version: 2.3.2(ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@6.0.3)) ts-pattern: specifier: ^5.6.2 version: 5.7.1 + viem: + specifier: ^2.21.1 + version: 2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) zod: specifier: 3.23.8 version: 3.23.8 @@ -1047,13 +1075,13 @@ importers: version: 3.4.3 '@vercel/analytics': specifier: ^1.2.2 - version: 1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) + version: 1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) '@vercel/postgres': specifier: ^0.8.0 version: 0.8.0 '@vercel/toolbar': specifier: ^0.1.5 - version: 0.1.36(@vercel/analytics@1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1)) + version: 0.1.36(@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1)) '@visx/axis': specifier: ^3.0.0 version: 3.12.0(react@19.1.0) @@ -1158,10 +1186,10 @@ importers: version: 6.3.0 next: specifier: 15.2.4 - version: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + version: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) next-contentlayer: specifier: ^0.3.4 - version: 0.3.4(contentlayer@0.3.4(esbuild@0.25.4))(esbuild@0.25.4)(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.4(contentlayer@0.3.4(esbuild@0.25.4))(esbuild@0.25.4)(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-mdx-remote: specifier: ^5.0.0 version: 5.0.0(@types/react@19.1.0)(acorn@8.14.1)(react@19.1.0) @@ -1372,7 +1400,7 @@ importers: version: 5.1.5 next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)) + version: 4.2.3(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)) postcss: specifier: ^8.4.21 version: 8.5.3 @@ -1549,7 +1577,7 @@ importers: version: link:../../packages/icons '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) + version: 1.5.0(next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) cva: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1(typescript@5.8.3) @@ -1558,7 +1586,7 @@ importers: version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.1(react@19.1.0))(react@19.1.0) next: specifier: 15.1.6 - version: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) + version: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) next-mdx-remote: specifier: ^5.0.0 version: 5.0.0(@types/react@19.1.0)(acorn@8.14.1)(react@19.1.0) @@ -2139,7 +2167,7 @@ importers: version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.1(react@19.1.0))(react@19.1.0) next: specifier: 15.1.6 - version: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) + version: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) ts-pattern: specifier: ^5.3.1 version: 5.7.1 @@ -2254,7 +2282,7 @@ importers: version: link:../icons '@trpc/react-query': specifier: 10.45.2 - version: 10.45.2(@tanstack/react-query@5.75.5(react@18.3.1))(@trpc/client@11.1.0(@trpc/server@10.45.2)(typescript@5.8.3))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.45.2(@tanstack/react-query@5.90.2(react@18.3.1))(@trpc/client@11.1.0(@trpc/server@10.45.2)(typescript@5.8.3))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@trpc/server': specifier: 10.45.2 version: 10.45.2 @@ -2317,7 +2345,7 @@ importers: version: 6.15.0(bufferutil@4.0.9)(utf-8-validate@6.0.3) next: specifier: 15.1.6 - version: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4) + version: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4) qrcode.react: specifier: ^3.1.0 version: 3.2.0(react@18.3.1) @@ -2537,10 +2565,6 @@ packages: resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.24.7': - resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -3811,12 +3835,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.2.0': - resolution: {integrity: sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8025,6 +8043,21 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@spruceid/siwe-parser@2.1.2': + resolution: {integrity: sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ==} + + '@stablelib/binary@1.0.1': + resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} + + '@stablelib/int@1.0.1': + resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} + + '@stablelib/random@1.0.2': + resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} + + '@stablelib/wipe@1.0.1': + resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + '@storybook/addon-actions@8.3.0': resolution: {integrity: sha512-HvAc3fW979JVw8CSKXZMouvgrJ2BNLNWaUB8jNokQb3Us00P6igVKLwg/pBV8GBgDr5Ng4pHYqi/ZH+xzEYFFw==} peerDependencies: @@ -8548,6 +8581,9 @@ packages: '@tanstack/query-core@5.75.5': resolution: {integrity: sha512-kPDOxtoMn2Ycycb76Givx2fi+2pzo98F9ifHL/NFiahEDpDwSVW6o12PRuQ0lQnBOunhRG5etatAhQij91M3MQ==} + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + '@tanstack/query-devtools@5.28.10': resolution: {integrity: sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==} @@ -8576,6 +8612,11 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-router-devtools@1.120.2': resolution: {integrity: sha512-89qY5pKdIN6r5G0pHP92mLvorzd7rUlHjvCkjNYOyYduxGQ8a703Y7Fp/RqXibwhjvZcet6BR2IrEhIqg9Yjhw==} engines: {node: '>=12'} @@ -8597,6 +8638,13 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.8': resolution: {integrity: sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==} peerDependencies: @@ -8668,6 +8716,10 @@ packages: '@tanstack/store@0.7.0': resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.8': resolution: {integrity: sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==} @@ -8947,9 +8999,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -9995,6 +10044,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apg-js@4.4.0: + resolution: {integrity: sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==} + arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} @@ -10675,10 +10727,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chokidar@4.0.1: - resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} - engines: {node: '>= 14.16.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -12115,18 +12163,10 @@ packages: resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -16733,6 +16773,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hook-form@7.64.0: + resolution: {integrity: sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -17713,6 +17759,11 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + siwe@2.3.2: + resolution: {integrity: sha512-aSf+6+Latyttbj5nMu6GF3doMfv2UYj83hhwZgUF20ky6fTS83uVhkQABdIVnEuS8y1bBdk7p6ltb9SmlhTTlA==} + peerDependencies: + ethers: ^5.6.8 || ^6.0.8 + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -19001,6 +19052,9 @@ packages: engines: {node: '>=8'} hasBin: true + valid-url@1.0.9: + resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -19758,7 +19812,7 @@ snapshots: '@babel/traverse': 7.27.1(supports-color@5.5.0) '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -19820,13 +19874,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.24.7(supports-color@5.5.0)': - dependencies: - '@babel/traverse': 7.27.1(supports-color@5.5.0) - '@babel/types': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-imports@7.27.1(supports-color@5.5.0)': dependencies: '@babel/traverse': 7.27.1(supports-color@5.5.0) @@ -21307,11 +21354,6 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.2.0(eslint@9.14.0(jiti@2.4.2))': - dependencies: - eslint: 9.14.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.1 - '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@2.4.2))': dependencies: eslint: 9.14.0(jiti@2.4.2) @@ -21332,7 +21374,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -21356,7 +21398,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -22261,6 +22303,10 @@ snapshots: dependencies: react-hook-form: 7.58.1(react@19.1.0) + '@hookform/resolvers@3.10.0(react-hook-form@7.64.0(react@19.1.0))': + dependencies: + react-hook-form: 7.64.0(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -23069,6 +23115,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@metamask/sdk-communication-layer@0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.14)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + bufferutil: 4.0.9 + cross-fetch: 4.1.0 + date-fns: 2.30.0 + debug: 4.4.0 + eciesjs: 0.4.14 + eventemitter2: 6.4.9 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + utf-8-validate: 5.0.10 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + '@metamask/sdk-install-modal-web@0.32.0': dependencies: '@paulmillr/qr': 0.2.1 @@ -23100,6 +23161,33 @@ snapshots: - supports-color - utf-8-validate + '@metamask/sdk@0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.27.1 + '@metamask/onboarding': 1.0.1 + '@metamask/providers': 16.1.0 + '@metamask/sdk-communication-layer': 0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.14)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.0 + '@paulmillr/qr': 0.2.1 + bowser: 2.11.0 + cross-fetch: 4.1.0 + debug: 4.4.0 + eciesjs: 0.4.14 + eth-rpc-errors: 4.0.3 + eventemitter2: 6.4.9 + obj-multiplex: 1.0.0 + pump: 3.0.2 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tslib: 2.8.1 + util: 0.12.5 + uuid: 8.3.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@metamask/superstruct@3.2.1': {} '@metamask/utils@5.0.2': @@ -26723,7 +26811,7 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - use-sync-external-store: 1.2.0(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) '@react-aria/color@3.0.0-rc.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -27675,6 +27763,17 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-common@1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + big.js: 6.2.2 + dayjs: 1.11.13 + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@reown/appkit-controllers@1.7.3(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@reown/appkit-common': 1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -27709,6 +27808,40 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-controllers@1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-wallet': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + valtio: 1.13.2(@types/react@19.1.0)(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit-polyfills@1.7.3': dependencies: buffer: 6.0.3 @@ -27749,6 +27882,42 @@ snapshots: - valtio - zod + '@reown/appkit-scaffold-ui@1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1))(zod@3.23.8)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-controllers': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-ui': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-utils': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1))(zod@3.23.8) + '@reown/appkit-wallet': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10) + lit: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - valtio + - zod + '@reown/appkit-ui@1.7.3(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@reown/appkit-common': 1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -27783,6 +27952,40 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-ui@1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-controllers': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-wallet': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10) + lit: 3.1.0 + qrcode: 1.5.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit-utils@1.7.3(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(valtio@1.13.2(@types/react@19.1.0)(react@19.1.0))(zod@3.23.8)': dependencies: '@reown/appkit-common': 1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -27820,6 +28023,43 @@ snapshots: - utf-8-validate - zod + '@reown/appkit-utils@1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1))(zod@3.23.8)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-controllers': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-polyfills': 1.7.3 + '@reown/appkit-wallet': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@walletconnect/logger': 2.1.2 + '@walletconnect/universal-provider': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + valtio: 1.13.2(@types/react@19.1.0)(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@reown/appkit-wallet@1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)': dependencies: '@reown/appkit-common': 1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -27831,6 +28071,17 @@ snapshots: - typescript - utf-8-validate + '@reown/appkit-wallet@1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-polyfills': 1.7.3 + '@walletconnect/logger': 2.1.2 + zod: 3.23.8 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + '@reown/appkit@1.7.3(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@reown/appkit-common': 1.7.3(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -27872,6 +28123,47 @@ snapshots: - utf-8-validate - zod + '@reown/appkit@1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@reown/appkit-common': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-controllers': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-polyfills': 1.7.3 + '@reown/appkit-scaffold-ui': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1))(zod@3.23.8) + '@reown/appkit-ui': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@reown/appkit-utils': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1))(zod@3.23.8) + '@reown/appkit-wallet': 1.7.3(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@walletconnect/types': 2.19.2 + '@walletconnect/universal-provider': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + bs58: 6.0.0 + valtio: 1.13.2(@types/react@19.1.0)(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@repeaterjs/repeater@3.0.6': {} '@rollup/plugin-inject@5.0.5(rollup@4.40.2)': @@ -27972,6 +28264,16 @@ snapshots: - utf-8-validate - zod + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + events: 3.3.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 @@ -27982,6 +28284,16 @@ snapshots: - utf-8-validate - zod + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@safe-global/safe-gateway-typescript-sdk': 3.23.1 + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} '@scure/base@1.0.0': {} @@ -28042,6 +28354,26 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@spruceid/siwe-parser@2.1.2': + dependencies: + '@noble/hashes': 1.8.0 + apg-js: 4.4.0 + uri-js: 4.4.1 + valid-url: 1.0.9 + + '@stablelib/binary@1.0.1': + dependencies: + '@stablelib/int': 1.0.1 + + '@stablelib/int@1.0.1': {} + + '@stablelib/random@1.0.2': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/wipe@1.0.1': {} + '@storybook/addon-actions@8.3.0(storybook@8.3.0(bufferutil@4.0.9)(utf-8-validate@6.0.3))': dependencies: '@storybook/global': 5.0.0 @@ -28625,6 +28957,8 @@ snapshots: '@tanstack/query-core@5.75.5': {} + '@tanstack/query-core@5.90.2': {} + '@tanstack/query-devtools@5.28.10': {} '@tanstack/query-devtools@5.74.7': {} @@ -28646,14 +28980,19 @@ snapshots: '@tanstack/query-core': 5.29.0 react: 19.1.0 - '@tanstack/react-query@5.75.5(react@18.3.1)': + '@tanstack/react-query@5.75.5(react@19.1.0)': dependencies: '@tanstack/query-core': 5.75.5 + react: 19.1.0 + + '@tanstack/react-query@5.90.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.2 react: 18.3.1 - '@tanstack/react-query@5.75.5(react@19.1.0)': + '@tanstack/react-query@5.90.2(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.75.5 + '@tanstack/query-core': 5.90.2 react: 19.1.0 '@tanstack/react-router-devtools@1.120.2(@tanstack/react-router@1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.119.0)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)': @@ -28686,6 +29025,12 @@ snapshots: react-dom: 19.1.0(react@19.1.0) use-sync-external-store: 1.5.0(react@19.1.0) + '@tanstack/react-table@8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@tanstack/react-virtual@3.13.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/virtual-core': 3.13.8 @@ -28765,6 +29110,8 @@ snapshots: '@tanstack/store@0.7.0': {} + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.13.8': {} '@tanstack/virtual-file-routes@1.115.0': {} @@ -28889,19 +29236,19 @@ snapshots: '@trpc/server': 11.1.0(patch_hash=j772m5gatolhdil2x65xum5qqi)(typescript@5.8.3) typescript: 5.8.3 - '@trpc/next@10.45.2(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@trpc/next@10.45.2(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@11.1.0(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@10.45.2)(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@tanstack/react-query': 5.75.5(react@19.1.0) + '@tanstack/react-query': 5.90.2(react@19.1.0) '@trpc/client': 10.45.2(@trpc/server@10.45.2) - '@trpc/react-query': 11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + '@trpc/react-query': 11.1.0(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) '@trpc/server': 10.45.2 next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@trpc/react-query@10.45.2(@tanstack/react-query@5.75.5(react@18.3.1))(@trpc/client@11.1.0(@trpc/server@10.45.2)(typescript@5.8.3))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@trpc/react-query@10.45.2(@tanstack/react-query@5.90.2(react@18.3.1))(@trpc/client@11.1.0(@trpc/server@10.45.2)(typescript@5.8.3))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-query': 5.75.5(react@18.3.1) + '@tanstack/react-query': 5.90.2(react@18.3.1) '@trpc/client': 11.1.0(@trpc/server@10.45.2)(typescript@5.8.3) '@trpc/server': 10.45.2 react: 18.3.1 @@ -28916,9 +29263,9 @@ snapshots: react-dom: 19.1.0(react@19.1.0) typescript: 5.8.3 - '@trpc/react-query@11.1.0(@tanstack/react-query@5.75.5(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': + '@trpc/react-query@11.1.0(@tanstack/react-query@5.90.2(react@19.1.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': dependencies: - '@tanstack/react-query': 5.75.5(react@19.1.0) + '@tanstack/react-query': 5.90.2(react@19.1.0) '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/server': 10.45.2 react: 19.1.0 @@ -29096,8 +29443,6 @@ snapshots: '@types/estree@1.0.5': {} - '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} '@types/express-serve-static-core@4.19.5': @@ -29549,23 +29894,23 @@ snapshots: mdast-util-to-string: 3.2.0 unist-util-visit: 4.1.2 - '@vercel/analytics@1.5.0(next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': + '@vercel/analytics@1.5.0(next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': optionalDependencies: - next: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 svelte: 4.2.2 vue: 3.3.4 - '@vercel/analytics@1.5.0(next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': + '@vercel/analytics@1.5.0(next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': optionalDependencies: - next: 15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 svelte: 4.2.2 vue: 3.3.4 - '@vercel/analytics@1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': + '@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4)': optionalDependencies: - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 svelte: 4.2.2 vue: 3.3.4 @@ -29577,7 +29922,7 @@ snapshots: svelte: 4.2.2 vue: 3.3.4 - '@vercel/microfrontends@1.1.0(@vercel/analytics@1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1))': + '@vercel/microfrontends@1.1.0(@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1))': dependencies: ajv: 8.17.1 commander: 12.1.0 @@ -29588,8 +29933,8 @@ snapshots: nanoid: 3.3.11 path-to-regexp: 6.2.1 optionalDependencies: - '@vercel/analytics': 1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + '@vercel/analytics': 1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4) + next: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) vite: 6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1) @@ -29622,10 +29967,10 @@ snapshots: utf-8-validate: 6.0.3 ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@vercel/toolbar@0.1.36(@vercel/analytics@1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1))': + '@vercel/toolbar@0.1.36(@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1))': dependencies: '@tinyhttp/app': 1.3.0 - '@vercel/microfrontends': 1.1.0(@vercel/analytics@1.5.0(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1)) + '@vercel/microfrontends': 1.1.0(@vercel/analytics@1.5.0(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react@19.1.0)(svelte@4.2.2)(vue@3.3.4))(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1)) chokidar: 3.6.0 execa: 5.1.1 fast-glob: 3.3.2 @@ -29634,7 +29979,7 @@ snapshots: jsonc-parser: 3.3.1 strip-ansi: 6.0.1 optionalDependencies: - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 vite: 6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1) transitivePeerDependencies: @@ -30311,6 +30656,45 @@ snapshots: - utf-8-validate - zod + '@wagmi/connectors@5.8.1(@types/react@19.1.0)(@wagmi/core@2.17.1(@tanstack/query-core@5.90.2)(@types/react@19.1.0)(react@18.3.1)(typescript@5.6.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': + dependencies: + '@coinbase/wallet-sdk': 4.3.0 + '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@wagmi/core': 2.17.1(@tanstack/query-core@5.90.2)(@types/react@19.1.0)(react@18.3.1)(typescript@5.6.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)) + '@walletconnect/ethereum-provider': 2.20.0(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + cbw-sdk: '@coinbase/wallet-sdk@3.9.3' + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - supports-color + - uploadthing + - utf-8-validate + - zod + '@wagmi/core@2.17.1(@tanstack/query-core@5.29.0)(@types/react@19.1.0)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8))': dependencies: eventemitter3: 5.0.1 @@ -30326,6 +30710,21 @@ snapshots: - react - use-sync-external-store + '@wagmi/core@2.17.1(@tanstack/query-core@5.90.2)(@types/react@19.1.0)(react@18.3.1)(typescript@5.6.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.6.2) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + zustand: 5.0.0(@types/react@19.1.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.2 + typescript: 5.6.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + '@waku/core@0.0.26(@multiformats/multiaddr@12.2.0)(libp2p@0.46.18)': dependencies: '@noble/hashes': 1.8.0 @@ -30501,6 +30900,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.19.2 + '@walletconnect/utils': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/core@2.20.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -30544,6 +30986,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.20.0 + '@walletconnect/utils': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/environment@1.0.1': dependencies: tslib: 1.14.1 @@ -30588,6 +31073,46 @@ snapshots: - utf-8-validate - zod + '@walletconnect/ethereum-provider@2.20.0(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@reown/appkit': 1.7.3(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/sign-client': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/types': 2.20.0 + '@walletconnect/universal-provider': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/utils': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - react + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/events@1.0.1': dependencies: keyvaluestorage-interface: 1.0.0 @@ -30635,6 +31160,16 @@ snapshots: - bufferutil - utf-8-validate + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/safe-json': 1.0.2 + events: 3.3.0 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@walletconnect/keyvaluestorage@1.1.1': dependencies: '@walletconnect/safe-json': 1.0.2 @@ -30715,6 +31250,41 @@ snapshots: - utf-8-validate - zod + '@walletconnect/sign-client@2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@walletconnect/core': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.19.2 + '@walletconnect/utils': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/sign-client@2.20.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@walletconnect/core': 2.20.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8) @@ -30750,17 +31320,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/time@1.0.2': - dependencies: - tslib: 1.14.1 - - '@walletconnect/types@2.19.2': + '@walletconnect/sign-client@2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': dependencies: + '@walletconnect/core': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 - '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.20.0 + '@walletconnect/utils': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -30778,11 +31347,47 @@ snapshots: - '@vercel/blob' - '@vercel/kv' - aws4fetch + - bufferutil - db0 - ioredis + - typescript - uploadthing + - utf-8-validate + - zod - '@walletconnect/types@2.20.0': + '@walletconnect/time@1.0.2': + dependencies: + tslib: 1.14.1 + + '@walletconnect/types@2.19.2': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.20.0': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 @@ -30849,6 +31454,45 @@ snapshots: - utf-8-validate - zod + '@walletconnect/universal-provider@2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/types': 2.19.2 + '@walletconnect/utils': 2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/universal-provider@2.20.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@walletconnect/events': 1.0.1 @@ -30888,6 +31532,45 @@ snapshots: - utf-8-validate - zod + '@walletconnect/universal-provider@2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + '@walletconnect/types': 2.20.0 + '@walletconnect/utils': 2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/utils@2.19.2(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@noble/ciphers': 1.2.1 @@ -30931,6 +31614,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.19.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.19.2 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/utils@2.20.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8)': dependencies: '@noble/ciphers': 1.2.1 @@ -30974,6 +31700,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.20.0(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.20.0 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/window-getters@1.0.1': dependencies: tslib: 1.14.1 @@ -31065,6 +31834,11 @@ snapshots: abbrev@2.0.0: {} + abitype@1.0.8(typescript@5.6.2)(zod@3.23.8): + optionalDependencies: + typescript: 5.6.2 + zod: 3.23.8 + abitype@1.0.8(typescript@5.8.3)(zod@3.23.8): optionalDependencies: typescript: 5.8.3 @@ -31182,6 +31956,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apg-js@4.4.0: {} + arch@2.2.0: {} arctic@1.2.0(patch_hash=np2mbzzdnalh3n27syyoxx3zu4): @@ -31460,6 +32236,18 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.8 + babel-plugin-styled-components@2.1.4(@babel/core@7.27.1)(styled-components@5.3.11(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1))(supports-color@5.5.0): + dependencies: + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1) + lodash: 4.17.21 + picomatch: 2.3.1 + styled-components: 5.3.11(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - supports-color + babel-plugin-styled-components@2.1.4(@babel/core@7.27.1)(styled-components@5.3.11(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0))(supports-color@5.5.0): dependencies: '@babel/helper-annotate-as-pure': 7.27.1 @@ -32085,10 +32873,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chokidar@4.0.1: - dependencies: - readdirp: 4.0.2 - chokidar@4.0.3: dependencies: readdirp: 4.0.2 @@ -32355,6 +33139,26 @@ snapshots: - '@babel/core' - react-is + connectkit@1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.90.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)): + dependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + buffer: 6.0.3 + detect-browser: 5.3.0 + family: 0.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + qrcode: 1.5.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-state: 1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + resize-observer-polyfill: 1.5.1 + styled-components: 5.3.11(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + wagmi: 2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + transitivePeerDependencies: + - '@babel/core' + - react-is + consola@3.4.2: {} console-browserify@1.2.0: {} @@ -32888,6 +33692,10 @@ snapshots: dequal@2.0.3: {} + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1)): + dependencies: + valtio: 1.13.2(@types/react@19.1.0)(react@18.3.1) + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.0)(react@19.1.0)): dependencies: valtio: 1.13.2(@types/react@19.1.0)(react@19.1.0) @@ -33139,6 +33947,18 @@ snapshots: - supports-color - utf-8-validate + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + engine.io-parser@5.2.3: {} enhanced-resolve@5.17.1: @@ -33611,7 +34431,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 9.14.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -33630,7 +34450,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -33649,7 +34469,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 9.14.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -33682,7 +34502,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -33693,7 +34513,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -33704,7 +34524,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -33732,7 +34552,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -33761,7 +34581,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -33790,7 +34610,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.40.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@2.4.2)))(eslint@9.14.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -33970,12 +34790,8 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-visitor-keys@3.4.1: {} - eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} eslint@8.57.1: @@ -34023,7 +34839,7 @@ snapshots: eslint@9.14.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.2.0(eslint@9.14.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.14.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.7.0 @@ -34033,15 +34849,15 @@ snapshots: '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.0 - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 - eslint-visitor-keys: 4.2.0 + eslint-visitor-keys: 4.2.1 espree: 10.3.0 esquery: 1.5.0 esutils: 2.0.3 @@ -34049,7 +34865,7 @@ snapshots: file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - ignore: 5.2.4 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 @@ -34065,9 +34881,9 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) - eslint-visitor-keys: 4.2.0 + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.1 espree@9.6.1: dependencies: @@ -34379,7 +35195,7 @@ snapshots: extension-port-stream@3.0.0: dependencies: readable-stream: 3.6.2 - webextension-polyfill: 0.10.0 + webextension-polyfill: 0.12.0 external-editor@3.1.0: dependencies: @@ -34397,6 +35213,13 @@ snapshots: transitivePeerDependencies: - supports-color + family@0.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)): + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + wagmi: 2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + family@0.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8))(wagmi@2.15.2(@tanstack/query-core@5.29.0)(@tanstack/react-query@5.29.0(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.8)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@6.0.3)(viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8))(zod@3.23.8)): optionalDependencies: react: 19.1.0 @@ -34666,6 +35489,19 @@ snapshots: react: 19.1.0 react-dom: 19.1.1(react@19.1.0) + framer-motion@6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@motionone/dom': 10.12.0 + framesync: 6.0.1 + hey-listen: 1.0.8 + popmotion: 11.0.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + style-value-types: 5.0.0 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + framer-motion@6.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@motionone/dom': 10.12.0 @@ -36107,10 +36943,22 @@ snapshots: dependencies: ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + isows@1.0.6(ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@6.0.3)): dependencies: ws: 8.18.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) + isows@1.0.6(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + isows@1.0.6(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.3)): + dependencies: + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.3) + it-all@3.0.4: {} it-batched-bytes@2.0.4: @@ -38086,6 +38934,10 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mipd@0.0.7(typescript@5.6.2): + optionalDependencies: + typescript: 5.6.2 + mipd@0.0.7(typescript@5.8.3): optionalDependencies: typescript: 5.8.3 @@ -38216,12 +39068,12 @@ snapshots: - markdown-wasm - supports-color - next-contentlayer@0.3.4(contentlayer@0.3.4(esbuild@0.25.4))(esbuild@0.25.4)(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next-contentlayer@0.3.4(contentlayer@0.3.4(esbuild@0.25.4))(esbuild@0.25.4)(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@contentlayer/core': 0.3.4(esbuild@0.25.4) '@contentlayer/utils': 0.3.4 contentlayer: 0.3.4(esbuild@0.25.4) - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) transitivePeerDependencies: @@ -38258,13 +39110,13 @@ snapshots: - acorn - supports-color - next-sitemap@4.2.3(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)): + next-sitemap@4.2.3(next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.2 minimist: 1.2.8 - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) + next: 15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) next-sitemap@4.2.3(next@15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4)): dependencies: @@ -38274,7 +39126,7 @@ snapshots: minimist: 1.2.8 next: 15.3.0(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4) - next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4): + next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.4): dependencies: '@next/env': 15.1.6 '@swc/counter': 0.1.3 @@ -38284,7 +39136,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.27.1)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 15.1.6 '@next/swc-darwin-x64': 15.1.6 @@ -38301,7 +39153,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4): + next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4): dependencies: '@next/env': 15.1.6 '@swc/counter': 0.1.3 @@ -38328,7 +39180,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.1.6(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4): + next@15.1.6(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)(sass@1.80.4): dependencies: '@next/env': 15.1.6 '@swc/counter': 0.1.3 @@ -38355,7 +39207,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4): + next@15.2.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.80.4): dependencies: '@next/env': 15.2.4 '@swc/counter': 0.1.3 @@ -38787,6 +39639,20 @@ snapshots: outdent@0.5.0: {} + ox@0.6.7(typescript@5.6.2)(zod@3.23.8): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.9.0 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.0.8(typescript@5.6.2)(zod@3.23.8) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - zod + ox@0.6.7(typescript@5.8.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -38801,6 +39667,20 @@ snapshots: transitivePeerDependencies: - zod + ox@0.6.9(typescript@5.6.2)(zod@3.23.8): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.9.0 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.6.0 + abitype: 1.0.8(typescript@5.6.2)(zod@3.23.8) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - zod + ox@0.6.9(typescript@5.8.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -39867,6 +40747,10 @@ snapshots: dependencies: react: 19.1.0 + react-hook-form@7.64.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -40072,6 +40956,11 @@ snapshots: dependencies: react: 19.1.0 + react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-state@1.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -41619,7 +42508,7 @@ snapshots: sass@1.80.4: dependencies: '@parcel/watcher': 2.4.1 - chokidar: 4.0.1 + chokidar: 4.0.3 immutable: 4.3.7 source-map-js: 1.2.1 @@ -41880,6 +42769,14 @@ snapshots: sisteransi@1.0.5: {} + siwe@2.3.2(ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@6.0.3)): + dependencies: + '@spruceid/siwe-parser': 2.1.2 + '@stablelib/random': 1.0.2 + ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@6.0.3) + uri-js: 4.4.1 + valid-url: 1.0.9 + slash@3.0.0: {} slice-ansi@3.0.0: @@ -41931,6 +42828,17 @@ snapshots: - supports-color - utf-8-validate + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@5.5.0) + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -42245,9 +43153,27 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.1 + styled-components@5.3.11(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1): + dependencies: + '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/traverse': 7.27.1(supports-color@5.5.0) + '@emotion/is-prop-valid': 1.3.1 + '@emotion/stylis': 0.8.5 + '@emotion/unitless': 0.7.5 + babel-plugin-styled-components: 2.1.4(@babel/core@7.27.1)(styled-components@5.3.11(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.1.0)(react@18.3.1))(supports-color@5.5.0) + css-to-react-native: 3.2.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.1.0 + shallowequal: 1.1.0 + supports-color: 5.5.0 + transitivePeerDependencies: + - '@babel/core' + styled-components@5.3.11(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react-is@18.1.0)(react@19.1.0): dependencies: - '@babel/helper-module-imports': 7.24.7(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) '@babel/traverse': 7.27.1(supports-color@5.5.0) '@emotion/is-prop-valid': 1.3.1 '@emotion/stylis': 0.8.5 @@ -42263,17 +43189,19 @@ snapshots: transitivePeerDependencies: - '@babel/core' - styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): + styled-jsx@5.1.6(@babel/core@7.27.1)(react@18.3.1): dependencies: client-only: 0.0.1 - react: 19.1.0 + react: 18.3.1 optionalDependencies: '@babel/core': 7.27.1 - styled-jsx@5.1.6(react@18.3.1): + styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.1.0 + optionalDependencies: + '@babel/core': 7.27.1 stylis@4.2.0: {} @@ -42321,7 +43249,7 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 acorn: 8.14.1 - aria-query: 5.3.0 + aria-query: 5.3.2 axobject-query: 3.2.4 code-red: 1.0.4 css-tree: 2.3.1 @@ -43338,10 +44266,18 @@ snapshots: dependencies: react: 19.1.0 + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.4.0(react@19.1.0): dependencies: react: 19.1.0 + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.5.0(react@19.1.0): dependencies: react: 19.1.0 @@ -43389,11 +44325,22 @@ snapshots: kleur: 4.1.5 sade: 1.8.1 + valid-url@1.0.9: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.1.1 spdx-expression-parse: 3.0.1 + valtio@1.13.2(@types/react@19.1.0)(react@18.3.1): + dependencies: + derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.0)(react@18.3.1)) + proxy-compare: 2.6.0 + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.0 + react: 18.3.1 + valtio@1.13.2(@types/react@19.1.0)(react@19.1.0): dependencies: derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.0)(react@19.1.0)) @@ -43484,6 +44431,23 @@ snapshots: - utf-8-validate - zod + viem@2.23.2(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8): + dependencies: + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.6.2)(zod@3.23.8) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.7(typescript@5.6.2)(zod@3.23.8) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + viem@2.29.1(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8): dependencies: '@noble/curves': 1.8.2 @@ -43501,6 +44465,40 @@ snapshots: - utf-8-validate - zod + viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8): + dependencies: + '@noble/curves': 1.8.2 + '@noble/hashes': 1.7.2 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.6.2)(zod@3.23.8) + isows: 1.0.6(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.9(typescript@5.6.2)(zod@3.23.8) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.29.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.3)(zod@3.23.8): + dependencies: + '@noble/curves': 1.8.2 + '@noble/hashes': 1.7.2 + '@scure/bip32': 1.6.2 + '@scure/bip39': 1.5.4 + abitype: 1.0.8(typescript@5.8.3)(zod@3.23.8) + isows: 1.0.6(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.3)) + ox: 0.6.9(typescript@5.8.3)(zod@3.23.8) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-node@3.1.4(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -43670,6 +44668,44 @@ snapshots: - utf-8-validate - zod + wagmi@2.15.2(@tanstack/query-core@5.90.2)(@tanstack/react-query@5.90.2(react@18.3.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8): + dependencies: + '@tanstack/react-query': 5.90.2(react@18.3.1) + '@wagmi/connectors': 5.8.1(@types/react@19.1.0)(@wagmi/core@2.17.1(@tanstack/query-core@5.90.2)(@types/react@19.1.0)(react@18.3.1)(typescript@5.6.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.9)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + '@wagmi/core': 2.17.1(@tanstack/query-core@5.90.2)(@types/react@19.1.0)(react@18.3.1)(typescript@5.6.2)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)) + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + viem: 2.29.1(bufferutil@4.0.9)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/query-core' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - supports-color + - uploadthing + - utf-8-validate + - zod + walk-up-path@3.0.1: {} watchpack@2.4.2: @@ -43932,6 +44968,11 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.3 + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + ws@8.18.1(bufferutil@4.0.8)(utf-8-validate@6.0.3): optionalDependencies: bufferutil: 4.0.8 @@ -44129,6 +45170,12 @@ snapshots: optionalDependencies: react: 18.3.1 + zustand@5.0.0(@types/react@19.1.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + '@types/react': 19.1.0 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + zustand@5.0.0(@types/react@19.1.0)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)): optionalDependencies: '@types/react': 19.1.0