From 1584274cd7d1984c0d40088d844ef4c77fd877bf Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:46:12 +1000 Subject: [PATCH 01/26] feat: add framer motion to the ui package --- packages/ui/package.json | 2 ++ pnpm-lock.yaml | 61 ++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 26c42fb5..50eb8d1a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,6 +9,7 @@ "./lib/*": "./lib/*.ts", "./lib/toast": "./lib/toast.tsx", "./icons/*": "./icons/*.tsx", + "./motion/*": "./components/motion/*.tsx", "./icons/types": "./icons/types.ts", "./taiwind": "./tailwind.config.ts", "./postcss": "./postcss.config.js" @@ -48,6 +49,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "framer-motion": "^11.5.4", "lucide-react": "^0.376.0", "next": "14.2.5", "next-themes": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13c6eed7..c9181a50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -395,6 +395,9 @@ importers: cmdk: specifier: ^1.0.0 version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + framer-motion: + specifier: ^11.5.4 + version: 11.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lucide-react: specifier: ^0.376.0 version: 0.376.0(react@18.3.1) @@ -486,7 +489,7 @@ importers: version: 5.51.1(react@18.3.1) '@web3modal/wagmi': specifier: 4.2.3 - version: 4.2.3(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(@wagmi/connectors@5.1.8(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(@wagmi/core@2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(@wagmi/core@2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)) + version: 4.2.3(6xih3thcjuz6qgw7ybgdmzw6yy) viem: specifier: 2.21.1 version: 2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) @@ -1890,6 +1893,10 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2716,8 +2723,8 @@ packages: resolution: {integrity: sha512-7nakIjcRSs6781LkizYpIfXh1DYlkUDqyALciqz/BjFU/S97sVjZdL4cuKsG9NEarytE+f6p0Qbq2Bo1aocVUA==} engines: {node: '>=16'} - '@scure/base@1.1.6': - resolution: {integrity: sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==} + '@scure/base@1.1.8': + resolution: {integrity: sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg==} '@scure/bip32@1.3.3': resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} @@ -4783,6 +4790,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@11.5.4: + resolution: {integrity: sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -9703,8 +9724,8 @@ snapshots: '@metamask/utils@8.4.0': dependencies: '@ethereumjs/tx': 4.2.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.8 '@types/debug': 4.1.12 debug: 4.3.4 pony-cause: 2.1.11 @@ -9817,6 +9838,8 @@ snapshots: '@noble/hashes@1.4.0': {} + '@noble/hashes@1.5.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -10820,29 +10843,29 @@ snapshots: '@safe-global/safe-gateway-typescript-sdk@3.21.1': {} - '@scure/base@1.1.6': {} + '@scure/base@1.1.8': {} '@scure/bip32@1.3.3': dependencies: '@noble/curves': 1.3.0 '@noble/hashes': 1.3.3 - '@scure/base': 1.1.6 + '@scure/base': 1.1.8 '@scure/bip32@1.4.0': dependencies: '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@scure/base': 1.1.8 '@scure/bip39@1.2.2': dependencies: '@noble/hashes': 1.3.3 - '@scure/base': 1.1.6 + '@scure/base': 1.1.8 '@scure/bip39@1.3.0': dependencies: '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@scure/base': 1.1.8 '@sideway/address@4.1.5': dependencies: @@ -12112,8 +12135,8 @@ snapshots: lit: 3.1.0 qrcode: 1.5.3 - ? '@web3modal/wagmi@4.2.3(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(@wagmi/connectors@5.1.8(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(@wagmi/core@2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(@wagmi/core@2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))' - : dependencies: + '@web3modal/wagmi@4.2.3(6xih3thcjuz6qgw7ybgdmzw6yy)': + dependencies: '@wagmi/connectors': 5.1.8(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(@wagmi/core@2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@wagmi/core': 2.13.4(@tanstack/query-core@5.51.1)(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)) '@walletconnect/ethereum-provider': 2.13.0(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) @@ -13407,7 +13430,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -13419,12 +13442,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -13860,6 +13884,13 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@11.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.6.2 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fresh@0.5.2: {} fs-constants@1.0.0: {} @@ -17306,7 +17337,7 @@ snapshots: webauthn-p256@0.0.5: dependencies: '@noble/curves': 1.4.0 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 webextension-polyfill@0.10.0: {} From 6e5f373c6487461500a68405365e3a14c232082f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:46:29 +1000 Subject: [PATCH 02/26] feat: create circle framer motion shape --- .../ui/components/motion/shapes/circle.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/ui/components/motion/shapes/circle.tsx diff --git a/packages/ui/components/motion/shapes/circle.tsx b/packages/ui/components/motion/shapes/circle.tsx new file mode 100644 index 00000000..a24d9d8a --- /dev/null +++ b/packages/ui/components/motion/shapes/circle.tsx @@ -0,0 +1,74 @@ +import { motion, type MotionStyle, Variants } from 'framer-motion'; +import { forwardRef } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '../../../lib/utils'; + +const circleVariants = cva('', { + variants: { + variant: { + black: 'fill-black', + grey: 'fill-[#4A4A4A]', + green: 'fill-[#00F782]', + blue: 'fill-[#00A3F7]', + yellow: 'fill-[#F7DE00]', + red: 'fill-red-500', + }, + strokeVariant: { + black: 'stroke-black', + grey: 'stroke-[#4A4A4A]', + green: 'stroke-[#00F782]', + blue: 'stroke-[#00A3F7]', + yellow: 'stroke-[#F7DE00]', + red: 'stroke-red-500', + }, + glow: { + black: '', + grey: 'drop-shadow-[0_0_8px_#4A4A4A] glow-grey', + green: 'drop-shadow-[0_0_8px_#00F782] glow', + blue: 'drop-shadow-[0_0_8px_#00A3F7] glow-blue', + yellow: 'drop-shadow-[0_0_8px_#F7DE00] glow-yellow', + red: 'drop-shadow-[0_0_8px_F70000] glow-red', + }, + partial: { + '100': '', + '25': '[stroke-dasharray:25,100] [stroke-linecap:round]', + }, + }, + defaultVariants: { + variant: 'black', + strokeVariant: 'black', + glow: 'black', + partial: '100', + }, +}); + +type CircleVariantProps = VariantProps; + +type CircleProps = CircleVariantProps & { + cx: number | string; + cy: number | string; + r: number; + strokeWidth?: number; + className?: string; + variants?: Variants; + animate?: any; + style?: MotionStyle; +}; + +export const Circle = forwardRef( + ( + { cx, cy, r, strokeWidth, className, style, variant, strokeVariant, partial, glow, ...props }, + ref + ) => ( + + ) +); From 3d1ea41af8d0e234b8e325310b2505a521e37592 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:46:49 +1000 Subject: [PATCH 03/26] feat: create progress ui component --- packages/ui/components/motion/progress.tsx | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 packages/ui/components/motion/progress.tsx diff --git a/packages/ui/components/motion/progress.tsx b/packages/ui/components/motion/progress.tsx new file mode 100644 index 00000000..25e07e38 --- /dev/null +++ b/packages/ui/components/motion/progress.tsx @@ -0,0 +1,162 @@ +import { forwardRef, type HTMLAttributes, useMemo } from 'react'; +import { clsx } from 'clsx'; +import { motion } from 'framer-motion'; +import { Circle } from './shapes/circle'; +import { cn } from '../../lib/utils'; + +export enum PROGRESS_STATUS { + IDLE, + PENDING, + SUCCESS, + ERROR, +} + +type Step = { + text: Partial> & { [PROGRESS_STATUS.IDLE]: string }; + status: PROGRESS_STATUS; +}; + +interface ProgressProps extends HTMLAttributes { + steps: Array; +} + +const variants = { + idle: { opacity: 0, pathLength: 0 }, + pending: { opacity: 1, pathLength: 0.5 }, + error: { opacity: 1, pathLength: 0.5 }, + success: { opacity: 1, pathLength: 1 }, +}; + +const circleVariants = { + idle: { scale: 0.75 }, + pending: { scale: 1 }, + error: { scale: 1 }, + success: { scale: 1 }, +}; + +function ProgressStep({ + isFirst, + isLast, + circleRadius, + text, + status, +}: { + isFirst: boolean; + isLast: boolean; + status: PROGRESS_STATUS; + circleRadius: number; + text: Partial> & { [PROGRESS_STATUS.IDLE]: string }; +}) { + const circleVariant = useMemo(() => { + switch (status) { + case PROGRESS_STATUS.IDLE: + return 'grey' as const; + case PROGRESS_STATUS.PENDING: + return 'blue' as const; + case PROGRESS_STATUS.SUCCESS: + return 'green' as const; + case PROGRESS_STATUS.ERROR: + return 'red' as const; + } + }, [status]); + + const statusText = useMemo(() => { + switch (status) { + case PROGRESS_STATUS.IDLE: + return 'idle'; + case PROGRESS_STATUS.PENDING: + return 'pending'; + case PROGRESS_STATUS.SUCCESS: + return 'success'; + case PROGRESS_STATUS.ERROR: + return 'error'; + } + }, [status]); + + const height = circleRadius * 7; + const width = circleRadius * 7; + const x1 = width / 2; + + return ( +
+ + + + + + + + + + + + {text[status] ?? text[PROGRESS_STATUS.IDLE]} + +
+ ); +} + +export const Progress = forwardRef( + ({ steps, className, ...props }, ref) => { + return ( +
+ {steps.map(({ text, status }, i) => ( + + ))} +
+ ); + } +); +Progress.displayName = 'Progress'; From 186f5cda8dc153e7ee2ab0d3dd62d7b75b8f2879 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:47:25 +1000 Subject: [PATCH 04/26] fix: module ui spacing --- packages/ui/components/Module.tsx | 4 ++-- packages/ui/components/ModuleGrid.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/components/Module.tsx b/packages/ui/components/Module.tsx index 6289c8b9..3cafd400 100644 --- a/packages/ui/components/Module.tsx +++ b/packages/ui/components/Module.tsx @@ -46,8 +46,8 @@ const innerModuleVariants = cva( ), }, size: { - default: 'p-6', - lg: 'p-10 py-12', + default: 'p-4 sm:p-6', + lg: 'px-6 sm:px-10 py-8 sm:py-10', }, }, defaultVariants: { diff --git a/packages/ui/components/ModuleGrid.tsx b/packages/ui/components/ModuleGrid.tsx index 399d774d..0283a1ba 100644 --- a/packages/ui/components/ModuleGrid.tsx +++ b/packages/ui/components/ModuleGrid.tsx @@ -57,7 +57,7 @@ const ModuleGridTitle = forwardRef(
Date: Mon, 16 Sep 2024 16:48:02 +1000 Subject: [PATCH 05/26] feat: create AlertTooltip component in ui library --- packages/ui/components/ui/tooltip.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ui/components/ui/tooltip.tsx b/packages/ui/components/ui/tooltip.tsx index 89fcfa62..b6a6d6b9 100644 --- a/packages/ui/components/ui/tooltip.tsx +++ b/packages/ui/components/ui/tooltip.tsx @@ -12,6 +12,7 @@ import { /** @ts-ignore TS doesnt know what its talking about */ import { useDebounce } from '@uidotdev/usehooks'; import { cn } from '../../lib/utils'; +import { TriangleAlertIcon } from '../../icons/TriangleAlertIcon'; const TooltipRoot = PopoverPrimitive.Root; @@ -31,7 +32,7 @@ const TooltipContent = forwardRef< sideOffset={sideOffset} side="top" className={cn( - 'text-session-white animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-session-black border-px z-50 max-w-[90svw] overflow-hidden rounded-xl border border-[#1C2624] bg-opacity-50 px-4 py-2 text-sm shadow-xl outline-none sm:max-w-lg', + 'text-session-white animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-session-black border-px z-50 max-w-[90svw] overflow-hidden rounded-xl border border-[#1C2624] bg-opacity-50 px-4 py-2 text-sm shadow-xl outline-none md:max-w-lg', className )} {...props} @@ -92,4 +93,13 @@ const Tooltip = forwardRef, TooltipP } ); -export { Tooltip, TooltipRoot, TooltipContent, TooltipTrigger }; +const AlertTooltip = forwardRef< + ElementRef, + Omit +>((props, ref) => ( + + + +)); + +export { Tooltip, TooltipRoot, TooltipContent, TooltipTrigger, AlertTooltip }; From 908699e79f928f877b0480d37447a79268bbbaef Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:48:15 +1000 Subject: [PATCH 06/26] fix: alert dialog spacing issue on desktop --- packages/ui/components/ui/alert-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/ui/alert-dialog.tsx b/packages/ui/components/ui/alert-dialog.tsx index e8ffbf1f..d2cc2a0b 100644 --- a/packages/ui/components/ui/alert-dialog.tsx +++ b/packages/ui/components/ui/alert-dialog.tsx @@ -80,7 +80,7 @@ AlertDialogHeader.displayName = 'AlertDialogHeader'; const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
); From 5a72f8fae43544d8ec3881c43b24b7f2a61d691e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:48:57 +1000 Subject: [PATCH 07/26] feat: create getContractErrorName util function --- packages/contracts/util.ts | 39 +++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/contracts/util.ts b/packages/contracts/util.ts index 3c46ea93..4b790b54 100644 --- a/packages/contracts/util.ts +++ b/packages/contracts/util.ts @@ -1,5 +1,6 @@ -import { formatUnits, parseUnits } from 'viem'; +import { formatUnits, parseUnits, type SimulateContractErrorType, type TransactionExecutionErrorType } from 'viem'; import { SENT_DECIMALS } from './constants'; +import type { WriteContractErrorType } from 'wagmi/actions'; /** * Formats a value of type `bigint` as a string, using the {@link formatUnits} function and the {@link SENT_DECIMALS} constant. @@ -19,3 +20,39 @@ export function formatSENT(value: bigint): string { export function parseSENT(value: string): bigint { return parseUnits(value, SENT_DECIMALS); } + +/** + * Get a smart contract error name from a wagmi error. + * @param error - The error to get the name from. + * @returns The error name. + */ +export function getContractErrorName( + error: Error | SimulateContractErrorType | WriteContractErrorType | TransactionExecutionErrorType +) { + let reason = error.name; + + if (error?.cause && typeof error.cause === 'object') { + if ( + 'data' in error.cause && + error.cause.data && + typeof error.cause.data === 'object' && + 'abiItem' in error.cause.data && + error.cause.data.abiItem && + typeof error.cause.data.abiItem === 'object' && + 'name' in error.cause.data.abiItem && + typeof error.cause.data.abiItem.name === 'string' + ) { + reason = error.cause.data.abiItem.name; + } else if ( + 'cause' in error.cause && + typeof error.cause.cause === 'object' && + error.cause.cause && + 'name' in error.cause.cause && + typeof error.cause.cause.name === 'string' + ) { + reason = error.cause.cause.name; + } + } + + return reason.endsWith('Error') ? reason.slice(0, -5) : reason; +} From fae31217b53b4c090f617a57fbb7478819b9786f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:49:32 +1000 Subject: [PATCH 08/26] feat: add contract resetting and lifecycle status --- .../contracts/hooks/useContractWriteQuery.tsx | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/contracts/hooks/useContractWriteQuery.tsx b/packages/contracts/hooks/useContractWriteQuery.tsx index 5d11d118..d45e9e2a 100644 --- a/packages/contracts/hooks/useContractWriteQuery.tsx +++ b/packages/contracts/hooks/useContractWriteQuery.tsx @@ -31,6 +31,8 @@ export type ContractWriteQueryProps = { estimateContractWriteFee: () => void; /** Re-fetch the gas price and units of gas needed to write the contract */ refetchContractWriteFeeEstimate: () => void; + /** Reset the contract write query */ + resetContract: () => void; /** Estimate gas the amount of gas to make the contract write */ gasAmountEstimate: bigint | null; /** The current price of gas */ @@ -43,6 +45,8 @@ export type ContractWriteQueryProps = { writeStatus: WriteContractStatus; /** Status of the contract transaction */ transactionStatus: TransactionContractStatus; + /** Status of the whole contract call */ + contractCallStatus: WriteContractStatus; /** Contract simulation error */ simulateError: Error | SimulateContractErrorType | null; /** Contract write error */ @@ -86,8 +90,16 @@ export function useContractWriteQuery< const [estimateGasEnabled, setEstimateGasEnabled] = useState(false); const [simulateEnabled, setSimulateEnabled] = useState(false); const [contractArgs, setContractArgs] = useState(defaultArgs); + const [simulateStatusOverride, setSimulateStatusOverride] = + useState(null); - const { data: hash, error: writeError, status: writeStatus, writeContract } = useWriteContract(); + const { + data: hash, + error: writeError, + status: writeStatus, + writeContract, + reset, + } = useWriteContract(); const { error: transactionError, status: transactionStatus } = useWaitForTransactionReceipt({ hash, }); @@ -103,15 +115,26 @@ export function useContractWriteQuery< const { data, - status: simulateStatus, + status: simulateStatusRaw, error: simulateError, - refetch, + refetch: refetchRaw, } = useSimulateContract({ ...contractDetails, query: { enabled: simulateEnabled }, chainId: chains[chain].id, }); + const refetchSimulate = async () => { + setSimulateStatusOverride('pending'); + await refetchRaw(); + setSimulateStatusOverride(null); + }; + + const simulateStatus = useMemo( + () => simulateStatusOverride ?? simulateStatusRaw, + [simulateStatusOverride, simulateStatusRaw] + ); + const { estimateGasAmount, gasAmountEstimate, @@ -139,9 +162,28 @@ export function useContractWriteQuery< setSimulateEnabled(true); - void refetch(); + void refetchSimulate(); }; + const resetContract = () => { + setSimulateEnabled(false); + reset(); + }; + + const contractCallStatus = useMemo(() => { + if (!simulateEnabled) return 'idle'; + if (simulateStatus === 'error' || writeStatus === 'error' || transactionStatus === 'error') { + return 'error'; + } else if ( + simulateStatus === 'success' && + writeStatus === 'success' && + transactionStatus === 'success' + ) { + return 'success'; + } + return 'pending'; + }, [simulateEnabled, simulateStatus, writeStatus, transactionStatus]); + useEffect(() => { if (simulateStatus === 'success' && data?.request) { writeContract(data.request); @@ -160,12 +202,14 @@ export function useContractWriteQuery< simulateAndWriteContract, estimateContractWriteFee, refetchContractWriteFeeEstimate, + resetContract, fee, gasAmountEstimate, gasPrice, simulateStatus, writeStatus, transactionStatus, + contractCallStatus, simulateError, writeError, transactionError, From b39175967093f71eb95bad151f7a0c012d393285 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:49:48 +1000 Subject: [PATCH 09/26] feat: add SENT allowance contract resetting --- packages/contracts/hooks/SENT.tsx | 87 +++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/packages/contracts/hooks/SENT.tsx b/packages/contracts/hooks/SENT.tsx index c2c3cd82..868b388b 100644 --- a/packages/contracts/hooks/SENT.tsx +++ b/packages/contracts/hooks/SENT.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Address } from 'viem'; +import { Address, SimulateContractErrorType, TransactionExecutionErrorType } from 'viem'; import { useAccount } from 'wagmi'; import { ReadContractData } from 'wagmi/query'; import { SENTAbi } from '../abis'; @@ -10,7 +10,11 @@ import { useEffect, useMemo, useState } from 'react'; import { isProduction } from '@session/util/env'; import { formatBigIntTokenValue, formatNumber } from '@session/util/maths'; import { SENT_DECIMALS, SENT_SYMBOL } from '../constants'; -import { useContractWriteQuery, type WriteContractStatus } from './useContractWriteQuery'; +import { + GenericContractStatus, + useContractWriteQuery, + type WriteContractStatus, +} from './useContractWriteQuery'; import { useChain } from './useChain'; import type { CHAIN } from '../chains'; @@ -92,8 +96,13 @@ export function useAllowanceQuery({ export type UseProxyApprovalReturn = { approve: () => void; + approveWrite: () => void; + resetApprove: () => void; status: WriteContractStatus; - error: WriteContractErrorType | Error | null; + readStatus: GenericContractStatus; + simulateError: SimulateContractErrorType | Error | null; + writeError: WriteContractErrorType | Error | null; + transactionError: TransactionExecutionErrorType | Error | null; }; export function useProxyApproval({ @@ -104,25 +113,54 @@ export function useProxyApproval({ tokenAmount: bigint; }): UseProxyApprovalReturn { const [hasEnoughAllowance, setHasEnoughAllowance] = useState(false); + const [allowanceReadStatusOverride, setAllowanceReadStatusOverride] = + useState(null); + const chain = useChain(); const { address } = useAccount(); const { allowance, getAllowance, - status: readStatus, + status: readStatusRaw, + refetch: refetchRaw, } = useAllowanceQuery({ contractAddress, }); - const { simulateAndWriteContract, writeStatus, simulateError, writeError } = - useContractWriteQuery({ - contract: 'SENT', - functionName: 'approve', - chain, - }); + const refetchAllowance = async () => { + setAllowanceReadStatusOverride('pending'); + await refetchRaw(); + setAllowanceReadStatusOverride(null); + }; + + const readStatus = useMemo( + () => allowanceReadStatusOverride ?? readStatusRaw, + [allowanceReadStatusOverride, readStatusRaw] + ); + + const { + simulateAndWriteContract, + resetContract, + contractCallStatus, + simulateError, + writeError, + transactionError, + } = useContractWriteQuery({ + contract: 'SENT', + functionName: 'approve', + chain, + }); const approve = () => { - getAllowance(); + if (allowance) { + void refetchAllowance(); + } else { + getAllowance(); + } + }; + + const resetApprove = () => { + resetContract(); }; const approveWrite = () => { @@ -130,7 +168,7 @@ export function useProxyApproval({ throw new Error('Checking if current allowance is sufficient'); } - if (allowance >= tokenAmount) { + if (tokenAmount > BigInt(0) && allowance >= tokenAmount) { setHasEnoughAllowance(true); if (!isProduction()) { console.debug( @@ -149,23 +187,30 @@ export function useProxyApproval({ } if (!hasEnoughAllowance) { - return writeStatus; + return contractCallStatus; } if (readStatus === 'pending') { - return 'idle'; + return 'pending'; } else { - return writeStatus; + return contractCallStatus; } - }, [readStatus, writeStatus, hasEnoughAllowance]); - - const error = useMemo(() => simulateError ?? writeError, [simulateError, writeError]); + }, [readStatus, contractCallStatus, hasEnoughAllowance]); useEffect(() => { - if (readStatus === 'success') { + if (readStatus === 'success' && tokenAmount > BigInt(0)) { approveWrite(); } - }, [allowance, readStatus]); + }, [readStatus]); - return { approve, status, error }; + return { + approve, + approveWrite, + resetApprove, + status, + readStatus, + simulateError, + writeError, + transactionError, + }; } From 2b3432f94285d2df578a17bf55aa76067eb7c050 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:50:15 +1000 Subject: [PATCH 10/26] fix: add pubkey to nodes fetched via the rpc call --- apps/staking/lib/queries/getNode.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/staking/lib/queries/getNode.ts b/apps/staking/lib/queries/getNode.ts index 62113e57..0e3d8a6d 100644 --- a/apps/staking/lib/queries/getNode.ts +++ b/apps/staking/lib/queries/getNode.ts @@ -1,5 +1,6 @@ import { parseSessionNodeData } from '@/app/mystakes/modules/StakedNodesModule'; import { type Contributor, NODE_STATE, type ServiceNode } from '@session/sent-staking-js/client'; +import { StakedNode } from '@/components/StakedNodeCard'; type ExplorerResponse> = { id: string; @@ -97,10 +98,11 @@ export async function getNode({ address }: { address: string }) { const node = res.result.service_node_states[0] as ServiceNode | undefined; return node - ? { + ? ({ ...parseSessionNodeData(node, res.result.height), + pubKey: node.service_node_pubkey, state: NODE_STATE.RUNNING, - } + } satisfies StakedNode) : {}; } catch (error) { console.error('Error fetching service nodes:', error); From 708c10a21c107af63f2283d7a7dd379debdd7553 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:50:38 +1000 Subject: [PATCH 11/26] fix: add formatting to localized internal links --- apps/staking/lib/locale-defaults.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/staking/lib/locale-defaults.tsx b/apps/staking/lib/locale-defaults.tsx index 262405d9..9fec1a95 100644 --- a/apps/staking/lib/locale-defaults.tsx +++ b/apps/staking/lib/locale-defaults.tsx @@ -4,11 +4,11 @@ import { cn } from '@session/ui/lib/utils'; import { RichTranslationValues } from 'next-intl'; import Link from 'next/link'; import type { ReactNode } from 'react'; -import { FAUCET, NETWORK, SOCIALS, TICKER, URL } from './constants'; +import { FAUCET, NETWORK, SESSION_NODE_TIME_STATIC, SOCIALS, TICKER, URL } from './constants'; export const internalLink = (href: string, prefetch?: boolean) => { return (children: ReactNode) => ( - + {children} ); @@ -77,6 +77,7 @@ export const defaultTranslationElements = { 'Snapshot', 'text-session-green' ), + 'my-stakes-link': internalLink('/mystakes'), } satisfies RichTranslationValues; export const defaultTranslationVariables = { @@ -91,6 +92,8 @@ export const defaultTranslationVariables = { faucetDrip: FAUCET.DRIP, oxenProgram: 'Oxen Service Node Bonus program', notFoundContentType: 'page', + smallContributorLeaveRequestDelay: + SESSION_NODE_TIME_STATIC.SMALL_CONTRIBUTOR_EXIT_REQUEST_WAIT_TIME_DAYS, } satisfies RichTranslationValues; export const defaultTranslationValues: RichTranslationValues = { From 636686e57dc08b423209e0e0ba4eb818330c939d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:53:12 +1000 Subject: [PATCH 12/26] feat: create contract util functions for localization and fontend mappings --- apps/staking/lib/contracts.ts | 112 ++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 apps/staking/lib/contracts.ts diff --git a/apps/staking/lib/contracts.ts b/apps/staking/lib/contracts.ts new file mode 100644 index 00000000..02911ed2 --- /dev/null +++ b/apps/staking/lib/contracts.ts @@ -0,0 +1,112 @@ +import type { useTranslations } from 'next-intl'; +import type { SimulateContractErrorType, TransactionExecutionErrorType } from 'viem'; +import type { WriteContractErrorType } from 'wagmi/actions'; +import { getContractErrorName } from '@session/contracts'; +import { toast } from '@session/ui/lib/toast'; +import { GenericContractStatus, WriteContractStatus } from '@session/contracts/hooks/useContractWriteQuery'; +import { PROGRESS_STATUS } from '@session/ui/motion/progress'; + +/** + * Formats a localized contract error message based on the error type and the dictionary. + * @param dictionary - The dictionary to use for localization. + * @param parentDictKey - The parent dictionary key to use for localization. The key used in `useTranslations` + * @param errorGroupDictKey - The error group dictionary key to use for localization. + * @param error - The error to format. + * @returns The formatted error message. + */ +export const formatLocalizedContractErrorMessage = ({ + dictionary, + parentDictKey, + errorGroupDictKey, + error, +}: { + dictionary: ReturnType; + parentDictKey: string; + errorGroupDictKey: string; + error: Error | SimulateContractErrorType | WriteContractErrorType | TransactionExecutionErrorType; +}) => { + const parsedName = getContractErrorName(error); + const dictKey = `${errorGroupDictKey}.errors.${parsedName}`; + /** @ts-expect-error -- We handle the invalid key case in the if statement below */ + let reason = dictionary(dictKey); + if (reason === `${parentDictKey}.${dictKey}`) reason = parsedName; + /** @ts-expect-error -- This key should exist, but this logs an error and returns the key if it doesn't */ + return dictionary(`${errorGroupDictKey}.errorTemplate`, { reason }); +}; + +/** + * Formats a localized contract error messages based on the error types and the dictionary. This issued for all contract errors from a single contract lifecycle. + * @param dictionary - The dictionary to use for localization. + * @param parentDictKey - The parent dictionary key to use for localization. The key used in `useTranslations` + * @param errorGroupDictKey - The error group dictionary key to use for localization. + * @param error - The error to format. + * @returns The formatted error message. + */ +export const formatAndHandleLocalizedContractErrorMessages = ({ + dictionary, + dictionaryGeneral, + parentDictKey, + errorGroupDictKey, + simulateError, + writeError, + transactionError, +}: { + dictionary: ReturnType; + dictionaryGeneral: ReturnType; + parentDictKey: string; + errorGroupDictKey: string; + simulateError?: SimulateContractErrorType | Error | null; + writeError?: WriteContractErrorType | Error | null; + transactionError?: TransactionExecutionErrorType | Error | null; +}) => { + if (simulateError) { + toast.handleError(simulateError); + return formatLocalizedContractErrorMessage({ + dictionary, + parentDictKey, + errorGroupDictKey, + error: simulateError, + }); + } else if (writeError) { + toast.handleError(writeError); + return formatLocalizedContractErrorMessage({ + dictionary, + parentDictKey, + errorGroupDictKey, + error: writeError, + }); + } else if (transactionError) { + toast.handleError(transactionError); + return formatLocalizedContractErrorMessage({ + dictionary, + parentDictKey, + errorGroupDictKey, + error: transactionError, + }); + } + return dictionaryGeneral('unknownError'); +}; + +/** + * Parses the contract status to a progress status. + * @param contractStatus - The contract status to parse. + * @returns The progress status. + * {@link PROGRESS_STATUS} + */ +export const parseContractStatusToProgressStatus = ( + contractStatus: GenericContractStatus | WriteContractStatus +) => { + switch (contractStatus) { + case 'error': + return PROGRESS_STATUS.ERROR; + + case 'pending': + return PROGRESS_STATUS.PENDING; + + case 'success': + return PROGRESS_STATUS.SUCCESS; + + default: + return PROGRESS_STATUS.IDLE; + } +}; From cf18967373d145cff93c03b1beec343cc69fae66 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:53:26 +1000 Subject: [PATCH 13/26] chore: update en.json --- apps/staking/locales/en.json | 193 ++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 83 deletions(-) diff --git a/apps/staking/locales/en.json b/apps/staking/locales/en.json index 0f65af39..8b4c823b 100644 --- a/apps/staking/locales/en.json +++ b/apps/staking/locales/en.json @@ -4,11 +4,11 @@ "loading": "Loading...", "or": "or", "and": "and", - "unknownError": "An unknown error occurred.", + "unknownError": "unknown error", "viewOnExplorer": "View on Explorer", "viewOnExplorerShort": "View", "you": "You", - "emptySlot": "EmptyContributorSlot", + "emptySlot": "Empty Contributor Slot", "notFound": "Not Found", "soon": "Soon" }, @@ -234,59 +234,38 @@ "amountClaimable": "Amount Claimable", "amountClaimableTooltip": "Amount of {tokenSymbol} rewards able to be claimed.", "buttons": { - "submit": "Claim", - "submitAria": "Claim {tokenAmount} {tokenSymbol} with a claim gas fee of {gasAmount} {gasTokenSymbol}" + "submit": "Claim {tokenAmount}", + "submitAria": "Claim {tokenAmount} with a claim gas fee of {gasAmount} {gasTokenSymbol}" }, "alert": { "gasFetchFailedUpdateBalance": "Failed to fetch fee estimate for updating rewards balance. Fee estimate may be inaccurate.", "gasFetchFailedClaimRewards": "Failed to fetch fee estimate for claiming rewards. Fee estimate may be inaccurate.", "highGas": "The price of Gas is higher than usual." - } + }, + "successToast": "Claimed {tokenAmount} on Arbitrum" }, "stage": { - "updateBalance": { - "simulate": { - "pending": "Validating rewards update parameters", - "success": "Validating rewards update parameters", - "error": "Validating rewards update parameters", - "errorTooltip": "Rewards update parameters were malformed, please refresh the page and try again" - }, - "write": { - "pending": "Updating rewards amount on Arbitrum", - "success": "Updating rewards amount on Arbitrum", - "error": "Updating rewards amount on Arbitrum", - "errorTooltip": "Rewards update was not successful on Arbitrum" - }, - "transaction": { - "pending": "Waiting for confirmation of rewards amount on Arbitrum", - "success": "Waiting for confirmation of rewards amount on Arbitrum", - "error": "Waiting for confirmation of rewards amount on Arbitrum", - "errorTooltip": "Rewards update was not successful on Arbitrum" + "balance": { + "idle": "Updating rewards amount on Arbitrum", + "pending": "Updating rewards amount on Arbitrum", + "success": "Updated rewards amount on Arbitrum", + "errorTemplate": "Failed to update rewards amount on Arbitrum, {reason}", + "errors": { + "UserRejectedRequest": "user rejected the request", + "NullAddress": "recipient address is invalid", + "RecipientRewardsTooLow": "rewards balance already updated", + "InvalidBLSSignature": "invalid BLS signature, try again later", + "InsufficientBLSSignatures": "insufficient BLS signatures, try again later" } }, - "claimRewards": { - "simulate": { - "pending": "Validating rewards claim", - "success": "Validating rewards claim", - "error": "Validating rewards claim", - "errorTooltip": "Rewards claim validation failed" - }, - "write": { - "pending": "Submitting rewards claim on Arbitrum", - "success": "Submitting rewards claim on Arbitrum", - "error": "Submitting rewards claim on Arbitrum", - "errorTooltip": "Rewards claim was not successful on Arbitrum" - }, - "transaction": { - "pending": "Waiting for confirmation of rewards claim", - "success": "Waiting for confirmation of rewards claim", - "error": "Waiting for confirmation of rewards claim", - "errorTooltip": "Rewards claim was not successful on Arbitrum" + "claim": { + "idle": "Claiming {tokenAmount} on Arbitrum", + "pending": "Claiming {tokenAmount} on Arbitrum", + "success": "Claimed {tokenAmount} on Arbitrum", + "errorTemplate": "Failed to claim rewards on Arbitrum, {reason}", + "errors": { + "UserRejectedRequest": "user rejected the request" } - }, - "done": { - "pending": "Claim Completed", - "success": "Claim Completed" } } }, @@ -329,45 +308,57 @@ "preparedAtTimestamp": "Registration Prepared", "preparedAtTimestampDescription": "When the node was prepared for registration", "button": { - "submit": "Register and Stake {amount} {tokenSymbol}" + "submit": "Register and Stake {amount}" }, "description": "Please start the registration process from your client or select from the list of open Session Node.", "notFound": { "description": "The registration you are looking for could not be found.", "foundOpenNode": "It looks like this node is already registered and open for staking:", - "foundRunningNode": "It looks like this node is already registered and running:" + "foundRunningNode": "It looks like this node is already registered and running, go to My Stakes to see your stakes:", + "foundRunningNodeOtherOperator": "It looks like this node is already registered and running for another operator:" }, + "notEnoughTokensAlert": "It looks like you don't have enough {tokenSymbol} to stake to this node. It looks like your wallet has {walletAmount}, please make sure you have at least {tokenAmount} in your wallet before registering.", "goToMyStakes": "The Session Node has been successfully registered and joined the network. It will appear on soon", "stage": { - "approve": { - "pending": "Requesting wallet permission to stake {tokenSymbol}", - "success": "Requesting wallet permission to stake {tokenSymbol}", - "error": "Requesting wallet permission to stake {tokenSymbol}", - "errorTooltip": "Wallet permission was not granted" - }, - "simulate": { - "pending": "Validating Session node registration parameters", - "success": "Validating Session node registration parameters", - "error": "Validating Session node registration parameters", - "errorTooltip": "Registration was malformed, please regenerate the registration" + "validate": { + "idle": "Validating registration", + "pending": "Validating registration", + "success": "Registration validated", + "errorTemplate": "Failed to validate registration, {reason}", + "errors": { + "InvalidBLSSignature": "invalid BLS signature, try again later" + } }, - "write": { - "pending": "Requesting Session node registration stake from wallet", - "success": "Requesting Session node registration stake from wallet", - "error": "Requesting Session node registration stake from wallet", - "errorTooltip": "Session node registration stake was not approved" + "approve": { + "idle": "Requesting permission to stake {tokenAmount}", + "pending": "Requesting permission to stake {tokenAmount} waiting for user approval", + "success": "Permission to stake {tokenSymbol} granted", + "errorTemplate": "Wallet permission to stake {tokenSymbol} failed, {reason}, please try again", + "errors": { + "UserRejectedRequest": "user rejected the request" + } }, - "transaction": { - "pending": "Submitting Session node registration to Arbitrum", - "success": "Submitting Session node registration to Arbitrum", - "error": "Submitting Session node registration to Arbitrum", - "errorTooltip": "Session node registration was not sent to Arbitrum" + "arbitrum": { + "idle": "Registering Session node on Arbitrum", + "pending": "Registering Session node on Arbitrum", + "success": "Registered Session node on Arbitrum", + "errorTemplate": "Arbitrum registration failed, {reason}", + "errors": { + "MaxContributorsExceeded": "max contributors exceeded", + "ContributionTotalMismatch": "stake does not match required amount", + "BLSPubkeyAlreadyExists": "node already registered with the same SN Key", + "InvalidBLSProofOfPossession": "BLS proof of possession is invalid", + "MaxPubkeyAggregationsExceeded": "block registration limit exceeded, please try again later", + "SafeERC20FailedOperation": "failed to stake {tokenSymbol}, please try again later" + } }, - "join": { - "pending": "Waiting for Session node to join the network", - "success": "Waiting for Session node to join the network", - "error": "Waiting for Session node to join the network", - "error_tooltip": "Session node was not able to join the network" + "network": { + "idle": "Adding Session node to the Session network", + "pending": "Adding Session node to the Session network waiting for confirmation", + "success": "Adding Session node to the Session network confirmed", + "errors": { + "template": "Session node failed to join the network, {reason}, please try again later" + } } } }, @@ -375,9 +366,11 @@ "title": "Stake to a Node", "contributors": "Shared Contributors", "contributorsTooltip": "The contributors currently staking to this node.", + "feeEstimate": "Fee Estimate", + "feeEstimateTooltip": "The amount of {gasTokenSymbol} this transaction will use. Learn more", "button": { - "submit": "Stake {amount} {tokenSymbol}", - "submitting": "Staking {amount} {tokenSymbol}...", + "submit": "Stake {amount}", + "submitting": "Staking {amount}...", "waitingForWallet": "Waiting for wallet authorization...", "submittingToArbitrum": "Submitting request to Arbitrum...", "success": "Successfully staked {amount} {tokenSymbol}." @@ -444,10 +437,27 @@ } }, "stage": { - "submit": "Submitting node exit to Arbitrum", - "success": "Node exit request submitted", - "error": "Failed to exit the node. Please try again later.", - "errorTooltip": "Failed to exit the node. Please try again later." + "arbitrum": { + "idle": "Session node exit on Arbitrum submitting", + "pending": "Session node exit on Arbitrum submitting", + "success": "Session node exit on Arbitrum submitted", + "errorTemplate": "Failed to submit node exit on Arbitrum, {reason}", + "errors": { + "UserRejectedRequest": "user rejected the request", + "SignatureExpired": "signature expired, try again later", + "BLSPubkeyDoesNotMatch": "BLS public key does not match", + "InvalidBLSSignature": "invalid BLS signature" + } + }, + "network": { + "idle": "Session node network exit confirming", + "pending": "Session node network exit confirming", + "success": "Session node network exit confirmed", + "errorTemplate": "Failed to confirm the node exit with the Session network, {reason}", + "errors": { + "UnableToReachNetwork": "unable to reach the Session network" + } + } } }, "requestExit": { @@ -482,10 +492,27 @@ } }, "stage": { - "submit": "Submitting node exit request to Arbitrum", - "success": "Node exit request submitted", - "error": "Failed to send node exit request. Please try again later.", - "errorTooltip": "Failed to send node exit request. Please try again later." + "arbitrum": { + "idle": "Session node exit request on Arbitrum submitting", + "pending": "Session node exit request on Arbitrum submitting", + "success": "Session node exit request on Arbitrum submitted", + "errorTemplate": "Failed to submit node exit request on Arbitrum, {reason}", + "errors": { + "UserRejectedRequest": "user rejected the request", + "CallerNotContributor": "caller not a node contributor", + "EarlierLeaveRequestMade": "leave request already made for this node", + "SmallContributorLeaveTooEarly": "small contributors can only initiate exit requests after the node has been running for {smallContributorLeaveRequestDelay} days" + } + }, + "network": { + "idle": "Session node exit request confirming", + "pending": "Session node exit request confirming", + "success": "Session node exit request confirmed", + "errorTemplate": "Failed to confirm the submission of the node exit request to the Session network, {reason}", + "errors": { + "UnableToReachNetwork": "unable to reach the Session network" + } + } } } } From 111fcfc70f6ebbd58fca496a2143930c19d2e1bf Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:54:10 +1000 Subject: [PATCH 14/26] fix: total staked tokens sum functions --- .../app/mystakes/modules/BalanceModule.tsx | 23 +++++------- apps/staking/components/NodeCard.tsx | 37 ++++++++++++++++++- apps/staking/components/StakedNodeCard.tsx | 16 ++++---- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/apps/staking/app/mystakes/modules/BalanceModule.tsx b/apps/staking/app/mystakes/modules/BalanceModule.tsx index 986e5d03..6a0fda7b 100644 --- a/apps/staking/app/mystakes/modules/BalanceModule.tsx +++ b/apps/staking/app/mystakes/modules/BalanceModule.tsx @@ -4,19 +4,18 @@ import { getVariableFontSizeForLargeModule, ModuleDynamicQueryText, } from '@/components/ModuleDynamic'; -import { getTotalStakedAmountForAddress } from '@/components/NodeCard'; +import { getTotalStakedAmountForAddressBigInt } from '@/components/NodeCard'; import type { ServiceNode } from '@session/sent-staking-js/client'; import { Module, ModuleTitle } from '@session/ui/components/Module'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; -import { Address } from 'viem'; +import type { Address } from 'viem'; import { useStakingBackendQueryWithParams } from '@/lib/sent-staking-backend-client'; import { getStakedNodes } from '@/lib/queries/getStakedNodes'; import { generateMockNodeData } from '@session/sent-staking-js/test'; import type { QUERY_STATUS } from '@/lib/query'; -import { formatSENTNumber } from '@session/contracts/hooks/SENT'; -import { DYNAMIC_MODULE } from '@/lib/constants'; +import { formatSENTBigInt } from '@session/contracts/hooks/SENT'; import { FEATURE_FLAG } from '@/lib/feature-flags'; import { useFeatureFlag } from '@/lib/feature-flags-client'; @@ -27,9 +26,11 @@ const getTotalStakedAmount = ({ nodes: Array; address: Address; }) => { - return nodes.reduce( - (acc, node) => acc + getTotalStakedAmountForAddress(node.contributors, address), - 0 + return formatSENTBigInt( + nodes.reduce( + (acc, node) => acc + getTotalStakedAmountForAddressBigInt(node.contributors, address), + BigInt(0) + ) ); }; @@ -75,10 +76,6 @@ export default function BalanceModule() { const titleFormat = useTranslations('modules.title'); const title = dictionary('title'); - const formattedTotalStakedAmount = useMemo(() => { - return `${formatSENTNumber(totalStakedAmount ?? 0, DYNAMIC_MODULE.SENT_ROUNDED_DECIMALS)}`; - }, [totalStakedAmount]); - return ( {titleFormat('format', { title })} @@ -94,10 +91,10 @@ export default function BalanceModule() { refetch, }} style={{ - fontSize: getVariableFontSizeForLargeModule(formattedTotalStakedAmount.length), + fontSize: getVariableFontSizeForLargeModule(totalStakedAmount?.length ?? 5), }} > - {formattedTotalStakedAmount} + {totalStakedAmount} ); diff --git a/apps/staking/components/NodeCard.tsx b/apps/staking/components/NodeCard.tsx index 628b7877..a7d74435 100644 --- a/apps/staking/components/NodeCard.tsx +++ b/apps/staking/components/NodeCard.tsx @@ -142,7 +142,14 @@ const ContributorIcon = ({ ); }; -export const getTotalStakedAmountForAddress = ( +/** + * @deprecated Use {@link getTotalStakedAmountForAddress} instead. + * Returns the total staked amount for a given address. + * @param contributors - The list of contributors. + * @param address - The address to check. + * @returns The total staked amount for the given address. + */ +export const getTotalStakedAmountForAddressNumber = ( contributors: Contributor[], address: string ): number => { @@ -153,6 +160,34 @@ export const getTotalStakedAmountForAddress = ( }, 0); }; +export const getTotalStakedAmountForAddressBigInt = ( + contributors: Contributor[], + address: string +): bigint => { + contributors = contributors.map(({ amount, address: contributorAddress }) => { + return { + amount: typeof amount === 'bigint' ? amount : BigInt(`${amount}`), + address: contributorAddress, + }; + }); + return contributors.reduce((acc, { amount, address: contributorAddress }) => { + return areHexesEqual(contributorAddress, address) ? acc + amount : acc; + }, BigInt(0)); +}; + +export const getTotalStakedAmountForAddress = ( + contributors: Contributor[], + address: string, + decimals?: number, + hideSymbol?: boolean +): string => { + return formatSENTBigInt( + getTotalStakedAmountForAddressBigInt(contributors, address), + decimals, + hideSymbol + ); +}; + type StakedNodeContributorListProps = HTMLAttributes & { contributors: Contributor[]; showEmptySlots?: boolean; diff --git a/apps/staking/components/StakedNodeCard.tsx b/apps/staking/components/StakedNodeCard.tsx index 00e9d980..a24f2c1c 100644 --- a/apps/staking/components/StakedNodeCard.tsx +++ b/apps/staking/components/StakedNodeCard.tsx @@ -17,7 +17,6 @@ import { StatusIndicator, statusVariants } from '@session/ui/components/StatusIn import { ArrowDownIcon } from '@session/ui/icons/ArrowDownIcon'; import { SpannerAndScrewdriverIcon } from '@session/ui/icons/SpannerAndScrewdriverIcon'; import { cn } from '@session/ui/lib/utils'; -import { formatNumber } from '@session/util/maths'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; import { cva, type VariantProps } from 'class-variance-authority'; import { useTranslations } from 'next-intl'; @@ -482,8 +481,8 @@ export const CollapsableButton = forwardRef< const StakedNodeCard = forwardRef< HTMLDivElement, - HTMLAttributes & { node: StakedNode } ->(({ className, node, ...props }, ref) => { + HTMLAttributes & { node: StakedNode; hideButton?: boolean } +>(({ className, node, hideButton, ...props }, ref) => { const dictionary = useTranslations('nodeCard.staked'); const generalDictionary = useTranslations('general'); const generalNodeDictionary = useTranslations('sessionNodes.general'); @@ -503,8 +502,8 @@ const StakedNodeCard = forwardRef< } = node; const formattedTotalStakedAmount = useMemo(() => { - if (!contributors || contributors.length === 0 || !address) return '0'; - return formatNumber(getTotalStakedAmountForAddress(contributors, address)); + if (!contributors || contributors.length === 0 || !address) return `0 ${SENT_SYMBOL}`; + return getTotalStakedAmountForAddress(contributors, address); }, [contributors, address]); const isSoloNode = contributors.length === 1; @@ -535,8 +534,7 @@ const StakedNodeCard = forwardRef< ) : null} {state === NODE_STATE.DECOMMISSIONED || state === NODE_STATE.DEREGISTERED || - state === NODE_STATE.UNLOCKED || - state === NODE_STATE.RUNNING ? ( + state === NODE_STATE.UNLOCKED ? ( {titleFormat('format', { title: stakingNodeDictionary('stakedBalance') })} - {formattedTotalStakedAmount} {SENT_SYMBOL} + {formattedTotalStakedAmount} {!isSoloNode ? ( @@ -607,7 +605,7 @@ const StakedNodeCard = forwardRef< {formatPercentage(operatorFee)} ) : null} - {state === NODE_STATE.RUNNING ? ( + {state === NODE_STATE.RUNNING && !hideButton ? ( isReadyToExit(node) ? ( ) : isRequestingToExit(node) ? ( From 25c728617084603a371981418b223149626df826 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:54:58 +1000 Subject: [PATCH 15/26] feat: use new progress component and lifecycle status logic for claiming tokens --- .../mystakes/modules/ClaimTokensModule.tsx | 202 +++++------------- 1 file changed, 52 insertions(+), 150 deletions(-) diff --git a/apps/staking/app/mystakes/modules/ClaimTokensModule.tsx b/apps/staking/app/mystakes/modules/ClaimTokensModule.tsx index 17f0048e..5510665e 100644 --- a/apps/staking/app/mystakes/modules/ClaimTokensModule.tsx +++ b/apps/staking/app/mystakes/modules/ClaimTokensModule.tsx @@ -18,23 +18,20 @@ import { formatBigIntTokenValue } from '@session/util/maths'; import { ETH_DECIMALS } from '@session/wallet/lib/eth'; import { LoadingText } from '@session/ui/components/loading-text'; import { QUERY, TICKER, URL } from '@/lib/constants'; -import useClaimRewards, { CLAIM_REWARDS_STATE } from '@/hooks/useClaimRewards'; -import { type ReactNode, useEffect, useMemo } from 'react'; +import useClaimRewards from '@/hooks/useClaimRewards'; +import { useEffect, useMemo } from 'react'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; import { externalLink } from '@/lib/locale-defaults'; -import { TriangleAlertIcon } from '@session/ui/icons/TriangleAlertIcon'; -import { Tooltip } from '@session/ui/ui/tooltip'; +import { AlertTooltip } from '@session/ui/ui/tooltip'; import { useStakingBackendQueryWithParams } from '@/lib/sent-staking-backend-client'; import { getRewardsClaimSignature } from '@/lib/queries/getRewardsClaimSignature'; -import type { WriteContractStatus } from '@session/contracts/hooks/useContractWriteQuery'; -import type { VariantProps } from 'class-variance-authority'; -import { StatusIndicator, statusVariants } from '@session/ui/components/StatusIndicator'; import type { Address } from 'viem'; import { Loading } from '@session/ui/components/loading'; import { useRemoteFeatureFlagQuery } from '@/lib/feature-flags-client'; import { REMOTE_FEATURE_FLAG } from '@/lib/feature-flags'; import { toast } from '@session/ui/lib/toast'; import { ClaimRewardsDisabledInfo } from '@/components/ClaimRewardsDisabledInfo'; +import { Progress, PROGRESS_STATUS } from '@session/ui/components/motion/progress'; export default function ClaimTokensModule() { const { address } = useWallet(); @@ -87,10 +84,10 @@ export default function ClaimTokensModule() { disabled={isDisabled} onClick={handleClick} > - + ['status'] { - switch (subStage) { - case 'error': - return 'red'; - case 'success': - return 'green'; - case 'pending': - return 'pending'; - default: - case 'idle': - return 'grey'; - } -} - -const dictionaryKey: Record = { - [CLAIM_REWARDS_STATE.SIMULATE_UPDATE_BALANCE]: 'updateBalance.simulate', - [CLAIM_REWARDS_STATE.WRITE_UPDATE_BALANCE]: 'updateBalance.write', - [CLAIM_REWARDS_STATE.TRANSACTION_UPDATE_BALANCE]: 'updateBalance.transaction', - [CLAIM_REWARDS_STATE.SIMULATE_CLAIM]: 'claimRewards.simulate', - [CLAIM_REWARDS_STATE.WRITE_CLAIM]: 'claimRewards.write', - [CLAIM_REWARDS_STATE.TRANSACTION_CLAIM]: 'claimRewards.transaction', -} as const; - -function getDictionaryKeyFromStageAndSubStage< - Stage extends CLAIM_REWARDS_STATE, - SubStage extends WriteContractStatus, ->({ - currentStage, - stage, - subStage, -}: { - currentStage: CLAIM_REWARDS_STATE; - stage: Stage; - subStage: SubStage; -}) { - return `${dictionaryKey[stage]}.${stage > currentStage || subStage === 'idle' ? 'pending' : subStage}`; -} - -function StageRow({ - currentStage, - stage, - subStage, - children, -}: { - currentStage: CLAIM_REWARDS_STATE; - stage: CLAIM_REWARDS_STATE; - subStage: WriteContractStatus; - children?: ReactNode; -}) { - const dictionary = useTranslations('modules.claim.stage'); - return ( - - currentStage - ? 'grey' - : stage < currentStage - ? 'green' - : undefined - } - /> - - {dictionary( - /** @ts-expect-error - TODO: Properly type this dictionary key construction function */ - children ?? getDictionaryKeyFromStageAndSubStage({ currentStage, stage, subStage }) - )} - - - ); -} - -function QueryStatusInformation({ - stage, - subStage, -}: { - stage: CLAIM_REWARDS_STATE; - subStage: WriteContractStatus; -}) { - return ( -
- - - - - - - - {stage === CLAIM_REWARDS_STATE.TRANSACTION_CLAIM && subStage === 'success' - ? 'done.success' - : 'done.pending'} - -
- ); -} - -const AlertTooltip = ({ tooltipContent }: { tooltipContent: ReactNode }) => { - return ( - - - - ); -}; - function ClaimTokensDialog({ formattedUnclaimedRewardsAmount, address, @@ -270,6 +135,7 @@ function ClaimTokensDialog({ excludedSigners: Array; }) { const dictionary = useTranslations('modules.claim.dialog'); + const dictionaryStage = useTranslations('modules.claim.stage'); const claimRewardsArgs = useMemo( () => ({ @@ -286,10 +152,12 @@ function ClaimTokensDialog({ claimFee, updateBalanceFee, estimateFee, - stage, - subStage, + updateRewardsBalanceStatus, + claimRewardsStatus, enabled, skipUpdateBalance, + updateRewardsBalanceErrorMessage, + claimRewardsErrorMessage, } = useClaimRewards(claimRewardsArgs); const feeEstimate = useMemo( @@ -312,10 +180,9 @@ function ClaimTokensDialog({ const isButtonDisabled = isDisabled || - (!skipUpdateBalance && - stage !== CLAIM_REWARDS_STATE.SIMULATE_UPDATE_BALANCE && - subStage !== 'idle') || - (skipUpdateBalance && stage !== CLAIM_REWARDS_STATE.SIMULATE_CLAIM && subStage !== 'idle'); + (skipUpdateBalance + ? claimRewardsStatus !== PROGRESS_STATUS.IDLE + : updateRewardsBalanceStatus !== PROGRESS_STATUS.IDLE); useEffect(() => { if (!isDisabled) { @@ -323,6 +190,12 @@ function ClaimTokensDialog({ } }, [address, rewards, blsSignature]); + useEffect(() => { + if (claimRewardsStatus === PROGRESS_STATUS.SUCCESS) { + toast.success(dictionary('successToast', { tokenAmount: formattedUnclaimedRewardsAmount })); + } + }, [claimRewardsStatus]); + return ( <>
@@ -353,7 +226,7 @@ function ClaimTokensDialog({ {formattedUnclaimedRewardsAmount}
- + - {enabled ? : null} + {enabled ? ( + + ) : null} ); From 1d76798ce4c9f98ea9cd523f41c8b43830defa7c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:55:22 +1000 Subject: [PATCH 16/26] feat: use new progress component and lifecycle status logic for node registration --- .../register/[nodeId]/NodeRegistration.tsx | 250 +++++++----------- apps/staking/hooks/useRegisterNode.tsx | 216 ++++++--------- 2 files changed, 180 insertions(+), 286 deletions(-) diff --git a/apps/staking/app/register/[nodeId]/NodeRegistration.tsx b/apps/staking/app/register/[nodeId]/NodeRegistration.tsx index 14922c8b..3e7f372d 100644 --- a/apps/staking/app/register/[nodeId]/NodeRegistration.tsx +++ b/apps/staking/app/register/[nodeId]/NodeRegistration.tsx @@ -12,28 +12,22 @@ import { useStakingBackendQueryWithParams } from '@/lib/sent-staking-backend-cli import type { LoadRegistrationsResponse } from '@session/sent-staking-js/client'; import { getPendingNodes } from '@/lib/queries/getPendingNodes'; import { QUERY, SESSION_NODE } from '@/lib/constants'; -import { formatBigIntTokenValue } from '@session/util/maths'; -import { SENT_DECIMALS, SENT_SYMBOL } from '@session/contracts'; import { getDateFromUnixTimestampSeconds } from '@session/util/date'; import { notFound } from 'next/navigation'; import { generateMockRegistrations } from '@session/sent-staking-js/test'; -import useRegisterNode, { REGISTER_STAGE } from '@/hooks/useRegisterNode'; -import { StatusIndicator, statusVariants } from '@session/ui/components/StatusIndicator'; -import type { VariantProps } from 'class-variance-authority'; +import useRegisterNode from '@/hooks/useRegisterNode'; import { useQuery } from '@tanstack/react-query'; import { getNode } from '@/lib/queries/getNode'; import { type StakedNode, StakedNodeCard } from '@/components/StakedNodeCard'; -import Link from 'next/link'; -import { Tooltip } from '@session/ui/ui/tooltip'; +import { AlertTooltip, Tooltip } from '@session/ui/ui/tooltip'; import { areHexesEqual } from '@session/util/string'; -import { isProduction } from '@/lib/env'; -import type { WriteContractStatus } from '@session/contracts/hooks/useContractWriteQuery'; import { toast } from '@session/ui/lib/toast'; import { RegistrationPausedInfo } from '@/components/RegistrationPausedInfo'; - import { useFeatureFlag, useRemoteFeatureFlagQuery } from '@/lib/feature-flags-client'; import { FEATURE_FLAG, REMOTE_FEATURE_FLAG } from '@/lib/feature-flags'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; +import { Progress, PROGRESS_STATUS } from '@session/ui/motion/progress'; +import { formatSENTBigInt } from '@session/contracts/hooks/SENT'; export default function NodeRegistration({ nodeId }: { nodeId: string }) { const showMockRegistration = useFeatureFlag(FEATURE_FLAG.MOCK_REGISTRATION); @@ -76,135 +70,6 @@ export default function NodeRegistration({ nodeId }: { nodeId: string }) { return isLoading ? : node ? : notFound(); } -function getStatusFromSubStage( - subStage: WriteContractStatus -): VariantProps['status'] { - switch (subStage) { - case 'error': - return 'red'; - case 'success': - return 'green'; - case 'pending': - return 'pending'; - default: - case 'idle': - return 'grey'; - } -} - -const stageDictionaryMap: Record = { - [REGISTER_STAGE.APPROVE]: 'approve', - [REGISTER_STAGE.SIMULATE]: 'simulate', - [REGISTER_STAGE.WRITE]: 'write', - [REGISTER_STAGE.TRANSACTION]: 'transaction', - [REGISTER_STAGE.JOIN]: 'join', -} as const; - -function getDictionaryKeyFromStageAndSubStage< - Stage extends REGISTER_STAGE, - SubStage extends WriteContractStatus, ->({ - currentStage, - stage, - subStage, -}: { - currentStage: REGISTER_STAGE; - stage: Stage; - subStage: SubStage; -}) { - return `${stageDictionaryMap[stage]}.${stage > currentStage || subStage === 'idle' ? 'pending' : subStage}`; -} - -function StageRow({ - currentStage, - stage, - subStage, -}: { - currentStage: REGISTER_STAGE; - stage: REGISTER_STAGE; - subStage: WriteContractStatus; -}) { - const dictionary = useTranslations('actionModules.register.stage'); - return ( - - currentStage - ? 'grey' - : stage < currentStage - ? 'green' - : undefined - } - /> - - {/** @ts-expect-error - TODO: Properly type this dictionary key construction function */} - {dictionary(getDictionaryKeyFromStageAndSubStage({ currentStage, stage, subStage }))} - - - ); -} - -function QueryStatusInformation({ - nodeId, - stage, - subStage, -}: { - nodeId: string; - stage: REGISTER_STAGE; - subStage: WriteContractStatus; -}) { - const dictionary = useTranslations('actionModules.register'); - - const { data: nodeRunning } = useQuery({ - queryKey: ['getNode', nodeId, 'checkRegistration'], - queryFn: async () => { - if (!isProduction) { - console.log('Checking if the node has joined the network'); - } - - const node = await getNode({ address: nodeId }); - - if (node && 'state' in node && node.state) { - return true; - } - throw new Error('Node has not joined the network yet'); - }, - // Allows for ~5 minutes of waiting - retry: 50, - // Retries every 30/n seconds or 5 seconds, whichever is longer - retryDelay: (attempt) => (isProduction ? Math.max((30 * 1000) / attempt, 5 * 1000) : 5000), - }); - - return ( -
- - - - - - {nodeRunning ? ( - - {dictionary.rich('goToMyStakes', { - link: () => ( - - My Stakes - - ), - })} - - ) : null} -
- ); -} - -// TODO - Add ability to set the stake amount when we build multi-contributor support function RegisterButton({ blsPubKey, blsSignature, @@ -225,21 +90,45 @@ function RegisterButton({ isRegistrationPausedFlagEnabled?: boolean; }) { const dictionary = useTranslations('actionModules.register'); - const { registerAndStake, stage, subStage, enabled } = useRegisterNode({ - blsPubKey, - blsSignature, - nodePubKey, - userSignature, - }); + const dictionaryStage = useTranslations('actionModules.register.stage'); + + const registerNodeArgs = useMemo( + () => ({ + blsPubKey, + blsSignature, + nodePubKey, + userSignature, + stakeAmount, + }), + [blsPubKey, blsSignature, nodePubKey, userSignature, stakeAmount] + ); + + const { + registerAndStake, + resetRegisterAndStake, + enabled, + allowanceReadStatus, + approveWriteStatus, + addBLSStatus, + approveErrorMessage, + addBLSErrorMessage, + } = useRegisterNode(registerNodeArgs); const handleClick = () => { if (isRegistrationPausedFlagEnabled) { toast.error(); } else { - registerAndStake(); + if (enabled) { + resetRegisterAndStake(); + registerAndStake(); + } else { + registerAndStake(); + } } }; + const tokenAmount = formatSENTBigInt(stakeAmount); + return ( <> - {enabled && (stage !== REGISTER_STAGE.APPROVE || subStage !== 'idle') ? ( - + {enabled ? ( + ) : null} ); @@ -267,12 +204,13 @@ export function NodeRegistrationForm({ const registerCardDictionary = useTranslations('nodeCard.pending'); const sessionNodeDictionary = useTranslations('sessionNodes.general'); const actionModuleSharedDictionary = useTranslations('actionModules.shared'); + const { tokenBalance } = useWallet(); const { enabled: isRegistrationPausedFlagEnabled, isLoading: isRemoteFlagLoading } = useRemoteFeatureFlagQuery(REMOTE_FEATURE_FLAG.DISABLE_NODE_REGISTRATION); const stakeAmount = BigInt(SESSION_NODE.FULL_STAKE_AMOUNT); - const stakeAmountString = formatBigIntTokenValue(stakeAmount, SENT_DECIMALS); + const stakeAmountString = formatSENTBigInt(stakeAmount); const preparationDate = getDateFromUnixTimestampSeconds(node.timestamp); const { data: runningNode, isLoading } = useQuery({ @@ -332,7 +270,17 @@ export function NodeRegistrationForm({ label={actionModuleSharedDictionary('stakeAmount')} tooltip={actionModuleSharedDictionary('stakeAmountDescription')} > - {stakeAmountString} {SENT_SYMBOL} + + {tokenBalance && tokenBalance < stakeAmount ? ( + + ) : null} + {stakeAmountString} + ) : null} { - const stage = useMemo(() => { - if ( - approveWriteStatus === 'success' && - addBLSSimulateStatus === 'success' && - addBLSWriteStatus === 'success' && - addBLSTransactionStatus === 'success' - ) { - return REGISTER_STAGE.JOIN; - } - - if ( - approveWriteStatus === 'success' && - addBLSSimulateStatus === 'success' && - addBLSWriteStatus === 'success' && - addBLSTransactionStatus !== 'success' - ) { - return REGISTER_STAGE.TRANSACTION; - } - - if ( - approveWriteStatus === 'success' && - addBLSSimulateStatus === 'success' && - addBLSWriteStatus !== 'success' && - addBLSTransactionStatus !== 'success' - ) { - return REGISTER_STAGE.WRITE; - } - - if ( - approveWriteStatus === 'success' && - addBLSSimulateStatus !== 'success' && - addBLSWriteStatus !== 'success' && - addBLSTransactionStatus !== 'success' - ) { - return REGISTER_STAGE.SIMULATE; - } - - if ( - approveWriteStatus !== 'success' && - addBLSSimulateStatus !== 'success' && - addBLSWriteStatus !== 'success' && - addBLSTransactionStatus !== 'success' - ) { - return REGISTER_STAGE.APPROVE; - } - return REGISTER_STAGE.APPROVE; - }, [approveWriteStatus, addBLSSimulateStatus, addBLSWriteStatus, addBLSTransactionStatus]); - - const subStage = useMemo(() => { - switch (stage) { - case REGISTER_STAGE.APPROVE: - return approveWriteStatus; - case REGISTER_STAGE.SIMULATE: - return addBLSSimulateStatus; - case REGISTER_STAGE.WRITE: - return addBLSWriteStatus; - case REGISTER_STAGE.TRANSACTION: - return addBLSTransactionStatus; - default: - return 'pending'; - } - }, [stage, approveWriteStatus, addBLSSimulateStatus, addBLSWriteStatus, addBLSTransactionStatus]); - - return { stage, subStage }; -}; export default function useRegisterNode({ blsPubKey, blsSignature, nodePubKey, userSignature, + stakeAmount, }: { blsPubKey: string; blsSignature: string; nodePubKey: string; userSignature: string; + stakeAmount: bigint; }) { const [enabled, setEnabled] = useState(false); - const dictionary = useTranslations('actionModules.register.stage'); + + const stageDictKey = 'actionModules.register.stage' as const; + const dictionary = useTranslations(stageDictKey); + const dictionaryGeneral = useTranslations('general'); + const { approve, - status: approveWriteStatus, - error: approveWriteError, + approveWrite, + resetApprove, + status: approveWriteStatusRaw, + readStatus, + writeError: approveWriteError, + simulateError: approveSimulateError, + transactionError: approveTransactionError, } = useProxyApproval({ // TODO: Create network provider to handle network specific logic contractAddress: addresses.ServiceNodeRewards.testnet, - tokenAmount: BigInt(SESSION_NODE.FULL_STAKE_AMOUNT), + tokenAmount: stakeAmount, }); const { addBLSPubKey, - writeStatus: addBLSWriteStatus, - transactionStatus: addBLSTransactionStatus, - simulateStatus: addBLSSimulateStatus, - simulateError, - writeError, + contractCallStatus: addBLSStatusRaw, + simulateError: addBLSSimulateError, + writeError: addBLSWriteError, + transactionError: addBLSTransactionError, } = useAddBLSPubKey({ blsPubKey, blsSignature, @@ -134,53 +57,76 @@ export default function useRegisterNode({ userSignature, }); - const { stage, subStage } = useRegisterStage({ - approveWriteStatus, - addBLSSimulateStatus, - addBLSWriteStatus, - addBLSTransactionStatus, - }); - const registerAndStake = () => { setEnabled(true); approve(); }; - // NOTE: Automatically triggers the write stage once the approval has succeeded - useEffect(() => { - if (enabled && approveWriteStatus === 'success') { - addBLSPubKey(); - } - }, [enabled, approveWriteStatus]); - - /** - * NOTE: All of these useEffects are required to inform the user of errors via the toaster - */ - useEffect(() => { - if (simulateError) { - toast.handleError(simulateError); - toast.error(dictionary('simulate.errorTooltip')); - } - }, [simulateError]); + const resetRegisterAndStake = () => { + if (addBLSStatusRaw !== 'idle') return; + setEnabled(false); + resetApprove(); + approveWrite(); + }; - useEffect(() => { - if (approveWriteError) { - toast.handleError(approveWriteError); - toast.error(dictionary('approve.errorTooltip')); - } - }, [approveWriteError]); + const approveErrorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'approve', + dictionary, + dictionaryGeneral, + simulateError: approveSimulateError, + writeError: approveWriteError, + transactionError: approveTransactionError, + }), + [approveSimulateError, approveWriteError, approveTransactionError] + ); + + const addBLSErrorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'arbitrum', + dictionary, + dictionaryGeneral, + simulateError: addBLSSimulateError, + writeError: addBLSWriteError, + transactionError: addBLSTransactionError, + }), + [addBLSSimulateError, addBLSWriteError, addBLSTransactionError] + ); + + const allowanceReadStatus = useMemo( + () => parseContractStatusToProgressStatus(readStatus), + [readStatus] + ); + + const approveWriteStatus = useMemo( + () => parseContractStatusToProgressStatus(approveWriteStatusRaw), + [approveWriteStatusRaw] + ); + + const addBLSStatus = useMemo( + () => parseContractStatusToProgressStatus(addBLSStatusRaw), + [addBLSStatusRaw] + ); + // NOTE: Automatically triggers the write stage once the approval has succeeded useEffect(() => { - if (writeError) { - toast.handleError(writeError); - toast.error(dictionary('write.errorTooltip')); + if (enabled && approveWriteStatusRaw === 'success') { + addBLSPubKey(); } - }, [writeError]); + }, [enabled, approveWriteStatusRaw]); return { registerAndStake, - stage, - subStage, + resetRegisterAndStake, + allowanceReadStatus, + approveWriteStatus, + approveErrorMessage, + addBLSErrorMessage, + addBLSStatus, enabled, }; } From 5f9b58620ab3e1e1fce31175dcd801c000028ab7 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:55:42 +1000 Subject: [PATCH 17/26] feat: use new progress component and lifecycle status logic for node exits --- .../StakedNode/NodeExitButtonDialog.tsx | 218 ++++------------- .../StakedNode/NodeInfoActionModuleBody.tsx | 81 +++++++ .../StakedNode/NodeRequestExitButton.tsx | 224 ++++-------------- apps/staking/hooks/useExitNode.tsx | 71 ++++++ apps/staking/hooks/useRequestNodeExit.tsx | 60 +++++ apps/staking/lib/constants.ts | 5 + 6 files changed, 311 insertions(+), 348 deletions(-) create mode 100644 apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx create mode 100644 apps/staking/hooks/useExitNode.tsx create mode 100644 apps/staking/hooks/useRequestNodeExit.tsx diff --git a/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx b/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx index 9527b35d..46b9bd6b 100644 --- a/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx +++ b/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx @@ -12,28 +12,20 @@ import { ButtonDataTestId } from '@/testing/data-test-ids'; import { Loading } from '@session/ui/components/loading'; import { type ReactNode, useEffect, useMemo } from 'react'; import Link from 'next/link'; -import { SOCIALS, TICKER, URL } from '@/lib/constants'; +import { SOCIALS } from '@/lib/constants'; import { Social } from '@session/ui/components/SocialLinkList'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; -import { useRemoveBLSPublicKeyWithSignature } from '@session/contracts/hooks/ServiceNodeRewards'; import { formatBigIntTokenValue } from '@session/util/maths'; import { ETH_DECIMALS } from '@session/wallet/lib/eth'; -import { getTotalStakedAmountForAddress, NodeContributorList } from '@/components/NodeCard'; -import { toast } from '@session/ui/lib/toast'; -import { ActionModuleRow } from '@/components/ActionModule'; -import { PubKey } from '@session/ui/components/PubKey'; -import { externalLink } from '@/lib/locale-defaults'; -import { LoadingText } from '@session/ui/components/loading-text'; -import { SENT_SYMBOL } from '@session/contracts'; +import { getTotalStakedAmountForAddress } from '@/components/NodeCard'; import { Button } from '@session/ui/ui/button'; -import type { - GenericContractStatus, - WriteContractStatus, -} from '@session/contracts/hooks/useContractWriteQuery'; -import { StatusIndicator } from '@session/ui/components/StatusIndicator'; import { NodeExitButton } from '@/components/StakedNode/NodeExitButton'; import { useStakingBackendQueryWithParams } from '@/lib/sent-staking-backend-client'; import { getNodeExitSignatures } from '@/lib/queries/getNodeExitSignatures'; +import NodeActionModuleInfo from '@/components/StakedNode/NodeInfoActionModuleBody'; +import { SENT_SYMBOL } from '@session/contracts'; +import { Progress, PROGRESS_STATUS } from '@session/ui/motion/progress'; +import useExitNode from '@/hooks/useExitNode'; export function NodeExitButtonDialog({ node }: { node: StakedNode }) { const dictionary = useTranslations('nodeCard.staked.exit'); @@ -103,9 +95,8 @@ function NodeExitContractWriteDialog({ excludedSigners?: Array; }) { const dictionary = useTranslations('nodeCard.staked.exit.dialog'); - const dictionaryStage = useTranslations('nodeCard.staked.exit.stage'); - const dictionaryActionModulesNode = useTranslations('actionModules.node'); - const sessionNodeDictionary = useTranslations('sessionNodes.general'); + const stageDictKey = 'nodeCard.staked.exit.stage' as const; + const dictionaryStage = useTranslations(stageDictKey); const { address } = useWallet(); const removeBlsPublicKeyWithSignatureArgs = useMemo( @@ -122,15 +113,11 @@ function NodeExitContractWriteDialog({ removeBLSPublicKeyWithSignature, fee, estimateContractWriteFee, - simulateStatus, - writeStatus, - transactionStatus, - estimateFeeError, - simulateError, - writeError, - transactionError, simulateEnabled, - } = useRemoveBLSPublicKeyWithSignature(removeBlsPublicKeyWithSignatureArgs); + resetContract, + status, + errorMessage, + } = useExitNode(removeBlsPublicKeyWithSignatureArgs); const feeEstimate = useMemo( () => (fee !== null ? formatBigIntTokenValue(fee ?? BigInt(0), ETH_DECIMALS, 18) : null), @@ -138,97 +125,33 @@ function NodeExitContractWriteDialog({ ); const stakedAmount = useMemo( - () => (address ? getTotalStakedAmountForAddress(node.contributors, address) : 0), + () => + address ? getTotalStakedAmountForAddress(node.contributors, address) : `0 ${SENT_SYMBOL}`, [node.contributors, address] ); const handleClick = () => { + if (simulateEnabled) { + resetContract(); + } removeBLSPublicKeyWithSignature(); }; const isDisabled = !blsPubKey || !timestamp || !blsSignature; - const isButtonDisabled = isDisabled || simulateEnabled; - useEffect(() => { if (!isDisabled) { estimateContractWriteFee(); } }, [node.contract_id]); - useEffect(() => { - if (estimateFeeError) { - toast.handleError(estimateFeeError); - } - }, [simulateError]); - - useEffect(() => { - if (simulateError) { - toast.handleError(simulateError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [simulateError]); - - useEffect(() => { - if (writeError) { - toast.handleError(writeError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [writeError]); - - useEffect(() => { - if (transactionError) { - toast.handleError(transactionError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [transactionError]); - return ( <> -
- - - - - - - - - - {node.contributors[0]?.address ? ( - - ) : null} - - - - {feeEstimate ? ( - `${feeEstimate} ${TICKER.ETH}` - ) : ( - - )} - - - - {stakedAmount} {SENT_SYMBOL} - -
+ {simulateEnabled ? ( - ) : null} ); } - -enum QUERY_STAGE { - IDLE, - PENDING, - SUCCESS, - ERROR, -} - -function QueryStatusInformation({ - simulateStatus, - writeStatus, - transactionStatus, -}: { - simulateStatus: GenericContractStatus; - writeStatus: WriteContractStatus; - transactionStatus: GenericContractStatus; -}) { - const dictionary = useTranslations('nodeCard.staked.exit.stage'); - const stage = useMemo(() => { - if (simulateStatus === 'error' || writeStatus === 'error' || transactionStatus === 'error') { - return QUERY_STAGE.ERROR; - } - - if ( - simulateStatus === 'success' && - writeStatus === 'success' && - transactionStatus === 'success' - ) { - return QUERY_STAGE.SUCCESS; - } - - if ( - simulateStatus === 'pending' || - writeStatus === 'pending' || - transactionStatus === 'pending' - ) { - return QUERY_STAGE.PENDING; - } - return QUERY_STAGE.IDLE; - }, [simulateStatus, writeStatus, transactionStatus]); - - return ( -
- - - - {stage === QUERY_STAGE.ERROR - ? dictionary('error') - : stage === QUERY_STAGE.SUCCESS - ? dictionary('success') - : dictionary('submit')} - - -
- ); -} diff --git a/apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx b/apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx new file mode 100644 index 00000000..4886ea79 --- /dev/null +++ b/apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx @@ -0,0 +1,81 @@ +import { ActionModuleRow } from '@/components/ActionModule'; +import { getTotalStakedAmountForAddress, NodeContributorList } from '@/components/NodeCard'; +import { PubKey } from '@session/ui/components/PubKey'; +import { externalLink } from '@/lib/locale-defaults'; +import { TICKER, URL } from '@/lib/constants'; +import { LoadingText } from '@session/ui/components/loading-text'; +import { SENT_SYMBOL } from '@session/contracts'; +import { useTranslations } from 'next-intl'; +import { useMemo } from 'react'; +import { StakedNode } from '@/components/StakedNodeCard'; +import { useWallet } from '@session/wallet/hooks/wallet-hooks'; + +export default function NodeActionModuleInfo({ + node, + feeEstimate, + feeEstimateText, +}: { + node: StakedNode; + feeEstimate?: string | null; + feeEstimateText?: string; +}) { + const { address } = useWallet(); + const dictionary = useTranslations('nodeCard.staked.requestExit.dialog.write'); + const dictionaryActionModulesNode = useTranslations('actionModules.node'); + const sessionNodeDictionary = useTranslations('sessionNodes.general'); + + const stakedAmount = useMemo( + () => + address ? getTotalStakedAmountForAddress(node.contributors, address) : `0 ${SENT_SYMBOL}`, + [node.contributors, address] + ); + + return ( +
+ + + + + + + + + + {node.contributors[0]?.address ? ( + + ) : null} + + {typeof feeEstimate !== 'undefined' ? ( + + + {feeEstimate ? ( + `${feeEstimate} ${TICKER.ETH}` + ) : ( + + )} + + + ) : null} + + {stakedAmount} + +
+ ); +} diff --git a/apps/staking/components/StakedNode/NodeRequestExitButton.tsx b/apps/staking/components/StakedNode/NodeRequestExitButton.tsx index 1b5be2a2..578b64cd 100644 --- a/apps/staking/components/StakedNode/NodeRequestExitButton.tsx +++ b/apps/staking/components/StakedNode/NodeRequestExitButton.tsx @@ -11,24 +11,11 @@ import { import { Button } from '@session/ui/ui/button'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { formatLocalizedTimeFromSeconds } from '@/lib/locale-client'; -import { SESSION_NODE_TIME, SOCIALS, TICKER, TOAST, URL } from '@/lib/constants'; -import { ActionModuleRow } from '@/components/ActionModule'; +import { SESSION_NODE_TIME, SOCIALS, URL } from '@/lib/constants'; import { externalLink } from '@/lib/locale-defaults'; -import { SENT_SYMBOL } from '@session/contracts'; -import type { - GenericContractStatus, - WriteContractStatus, -} from '@session/contracts/hooks/useContractWriteQuery'; -import { StatusIndicator } from '@session/ui/components/StatusIndicator'; -import type { SimulateContractErrorType, WriteContractErrorType } from 'viem'; -import { toast } from '@session/ui/lib/toast'; -import { collapseString } from '@session/util/string'; import { useChain } from '@session/contracts/hooks/useChain'; -import { useInitiateRemoveBLSPublicKey } from '@session/contracts/hooks/ServiceNodeRewards'; import { useWallet } from '@session/wallet/hooks/wallet-hooks'; -import { getTotalStakedAmountForAddress, NodeContributorList } from '@/components/NodeCard'; -import { PubKey } from '@session/ui/components/PubKey'; -import { LoadingText } from '@session/ui/components/loading-text'; +import { getTotalStakedAmountForAddress } from '@/components/NodeCard'; import { formatBigIntTokenValue } from '@session/util/maths'; import { ETH_DECIMALS } from '@session/wallet/lib/eth'; import { useRemoteFeatureFlagQuery } from '@/lib/feature-flags-client'; @@ -37,6 +24,10 @@ import { Loading } from '@session/ui/components/loading'; import Link from 'next/link'; import { Social } from '@session/ui/components/SocialLinkList'; import { ChevronsDownIcon } from '@session/ui/icons/ChevronsDownIcon'; +import { Progress, PROGRESS_STATUS } from '@session/ui/motion/progress'; +import useRequestNodeExit from '@/hooks/useRequestNodeExit'; +import NodeActionModuleInfo from '@/components/StakedNode/NodeInfoActionModuleBody'; +import { SENT_SYMBOL } from '@session/contracts'; enum EXIT_REQUEST_STATE { ALERT, @@ -176,25 +167,20 @@ function RequestNodeExitDialog({ node, onSubmit }: { node: StakedNode; onSubmit: } function RequestNodeExitContractWriteDialog({ node }: { node: StakedNode }) { + const stageDictKey = 'nodeCard.staked.requestExit.dialog.stage' as const; const dictionary = useTranslations('nodeCard.staked.requestExit.dialog.write'); - const dictionaryStage = useTranslations('nodeCard.staked.requestExit.dialog.stage'); - const dictionaryActionModulesNode = useTranslations('actionModules.node'); - const sessionNodeDictionary = useTranslations('sessionNodes.general'); + const dictionaryStage = useTranslations(stageDictKey); const { address } = useWallet(); const { initiateRemoveBLSPublicKey, fee, estimateContractWriteFee, - simulateStatus, - writeStatus, - transactionStatus, - estimateFeeError, - simulateError, - writeError, - transactionError, simulateEnabled, - } = useInitiateRemoveBLSPublicKey({ + resetContract, + status, + errorMessage, + } = useRequestNodeExit({ contractId: node.contract_id, }); @@ -204,97 +190,33 @@ function RequestNodeExitContractWriteDialog({ node }: { node: StakedNode }) { ); const stakedAmount = useMemo( - () => (address ? getTotalStakedAmountForAddress(node.contributors, address) : 0), + () => + address ? getTotalStakedAmountForAddress(node.contributors, address) : `0 ${SENT_SYMBOL}`, [node.contributors, address] ); const handleClick = () => { + if (simulateEnabled) { + resetContract(); + } initiateRemoveBLSPublicKey(); }; const isDisabled = !node.contract_id; - const isButtonDisabled = isDisabled || simulateEnabled; - useEffect(() => { if (!isDisabled) { estimateContractWriteFee(); } }, [node.contract_id]); - useEffect(() => { - if (estimateFeeError) { - toast.handleError(estimateFeeError); - } - }, [simulateError]); - - useEffect(() => { - if (simulateError) { - toast.handleError(simulateError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [simulateError]); - - useEffect(() => { - if (writeError) { - toast.handleError(writeError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [writeError]); - - useEffect(() => { - if (transactionError) { - toast.handleError(transactionError); - toast.error(dictionaryStage('errorTooltip')); - } - }, [transactionError]); - return ( <> -
- - - - - - - - - - {node.contributors[0]?.address ? ( - - ) : null} - - - - {feeEstimate ? ( - `${feeEstimate} ${TICKER.ETH}` - ) : ( - - )} - - - - {stakedAmount} {SENT_SYMBOL} - -
+ {simulateEnabled ? ( - ) : null} ); } - -const handleError = (error: Error | SimulateContractErrorType | WriteContractErrorType) => { - console.error(error); - if (error.message) { - toast.error( - collapseString(error.message, TOAST.ERROR_COLLAPSE_LENGTH, TOAST.ERROR_COLLAPSE_LENGTH) - ); - } -}; - -enum QUERY_STAGE { - IDLE, - PENDING, - SUCCESS, - ERROR, -} - -function QueryStatusInformation({ - simulateStatus, - writeStatus, - transactionStatus, -}: { - simulateStatus: GenericContractStatus; - writeStatus: WriteContractStatus; - transactionStatus: GenericContractStatus; -}) { - const dictionary = useTranslations('nodeCard.staked.requestExit.dialog.stage'); - const stage = useMemo(() => { - if (simulateStatus === 'error' || writeStatus === 'error' || transactionStatus === 'error') { - return QUERY_STAGE.ERROR; - } - - if ( - simulateStatus === 'success' && - writeStatus === 'success' && - transactionStatus === 'success' - ) { - return QUERY_STAGE.SUCCESS; - } - - if ( - simulateStatus === 'pending' || - writeStatus === 'pending' || - transactionStatus === 'pending' - ) { - return QUERY_STAGE.PENDING; - } - return QUERY_STAGE.IDLE; - }, [simulateStatus, writeStatus, transactionStatus]); - - return ( -
- - - - {stage === QUERY_STAGE.ERROR ? dictionary('error') : dictionary('submit')} - - -
- ); -} diff --git a/apps/staking/hooks/useExitNode.tsx b/apps/staking/hooks/useExitNode.tsx new file mode 100644 index 00000000..7f91649e --- /dev/null +++ b/apps/staking/hooks/useExitNode.tsx @@ -0,0 +1,71 @@ +import { useMemo } from 'react'; +import { useRemoveBLSPublicKeyWithSignature } from '@session/contracts/hooks/ServiceNodeRewards'; +import { useTranslations } from 'next-intl'; +import { + formatAndHandleLocalizedContractErrorMessages, + parseContractStatusToProgressStatus, +} from '@/lib/contracts'; + +type UseExitNodeParams = { + blsPubKey: string; + timestamp: number; + blsSignature: string; + excludedSigners?: Array; +}; + +export default function useExitNode({ + blsPubKey, + timestamp, + blsSignature, + excludedSigners, +}: UseExitNodeParams) { + const stageDictKey = 'nodeCard.staked.requestExit.dialog.stage' as const; + const dictionary = useTranslations(stageDictKey); + const dictionaryGeneral = useTranslations('general'); + + const { + removeBLSPublicKeyWithSignature, + fee, + estimateContractWriteFee, + contractCallStatus, + simulateError, + writeError, + transactionError, + simulateEnabled, + resetContract, + } = useRemoveBLSPublicKeyWithSignature({ + blsPubKey, + timestamp, + blsSignature, + excludedSigners, + }); + + const errorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'arbitrum', + dictionary, + dictionaryGeneral, + simulateError, + writeError, + transactionError, + }), + [simulateError, writeError, transactionError] + ); + + const status = useMemo( + () => parseContractStatusToProgressStatus(contractCallStatus), + [contractCallStatus] + ); + + return { + removeBLSPublicKeyWithSignature, + fee, + estimateContractWriteFee, + simulateEnabled, + resetContract, + errorMessage, + status, + }; +} diff --git a/apps/staking/hooks/useRequestNodeExit.tsx b/apps/staking/hooks/useRequestNodeExit.tsx new file mode 100644 index 00000000..61221aaf --- /dev/null +++ b/apps/staking/hooks/useRequestNodeExit.tsx @@ -0,0 +1,60 @@ +import { useMemo } from 'react'; +import { useInitiateRemoveBLSPublicKey } from '@session/contracts/hooks/ServiceNodeRewards'; +import { useTranslations } from 'next-intl'; +import { + formatAndHandleLocalizedContractErrorMessages, + parseContractStatusToProgressStatus, +} from '@/lib/contracts'; + +type UseRequestNodeExitParams = { + contractId: number; +}; + +export default function useRequestNodeExit({ contractId }: UseRequestNodeExitParams) { + const stageDictKey = 'nodeCard.staked.requestExit.dialog.stage' as const; + const dictionary = useTranslations(stageDictKey); + const dictionaryGeneral = useTranslations('general'); + + const { + initiateRemoveBLSPublicKey, + fee, + estimateContractWriteFee, + contractCallStatus, + simulateError, + writeError, + transactionError, + simulateEnabled, + resetContract, + } = useInitiateRemoveBLSPublicKey({ + contractId, + }); + + const errorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'arbitrum', + dictionary, + dictionaryGeneral, + simulateError, + writeError, + transactionError, + }), + [simulateError, writeError, transactionError] + ); + + const status = useMemo( + () => parseContractStatusToProgressStatus(contractCallStatus), + [contractCallStatus] + ); + + return { + initiateRemoveBLSPublicKey, + fee, + estimateContractWriteFee, + simulateEnabled, + resetContract, + errorMessage, + status, + }; +} diff --git a/apps/staking/lib/constants.ts b/apps/staking/lib/constants.ts index 223f01ed..381aa78f 100644 --- a/apps/staking/lib/constants.ts +++ b/apps/staking/lib/constants.ts @@ -118,6 +118,11 @@ export enum SESSION_NODE { MS_PER_BLOCK = 2 * 60 * 1000, } +export enum SESSION_NODE_TIME_STATIC { + /** 2 days in days */ + SMALL_CONTRIBUTOR_EXIT_REQUEST_WAIT_TIME_DAYS = 2, +} + enum SESSION_NODE_TIME_TESTNET { /** 1 day in seconds */ EXIT_REQUEST_TIME_SECONDS = 24 * 60 * 60, From deef29086f05562a34a24f44236f60d9f5e17384 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:56:19 +1000 Subject: [PATCH 18/26] feat: refactor claim rewards hook to simplify logic --- apps/staking/hooks/useClaimRewards.tsx | 305 +++++-------------------- 1 file changed, 59 insertions(+), 246 deletions(-) diff --git a/apps/staking/hooks/useClaimRewards.tsx b/apps/staking/hooks/useClaimRewards.tsx index 82f2cb6a..2643fa30 100644 --- a/apps/staking/hooks/useClaimRewards.tsx +++ b/apps/staking/hooks/useClaimRewards.tsx @@ -6,160 +6,13 @@ import { type UseUpdateRewardsBalanceQueryParams, } from '@session/contracts/hooks/ServiceNodeRewards'; import { useEffect, useMemo, useState } from 'react'; -import { toast } from '@session/ui/lib/toast'; import { useTranslations } from 'next-intl'; -import type { - GenericContractStatus, - WriteContractStatus, -} from '@session/contracts/hooks/useContractWriteQuery'; +import { getContractErrorName } from '@session/contracts'; -export enum CLAIM_REWARDS_STATE { - SIMULATE_UPDATE_BALANCE, - WRITE_UPDATE_BALANCE, - TRANSACTION_UPDATE_BALANCE, - SIMULATE_CLAIM, - WRITE_CLAIM, - TRANSACTION_CLAIM, -} - -const useClaimRewardsStage = ({ - updateBalanceSimulateStatus, - updateBalanceWriteStatus, - updateBalanceTransactionStatus, - claimSimulateStatus, - claimWriteStatus, - claimTransactionStatus, - skipUpdateBalance, -}: { - updateBalanceSimulateStatus: GenericContractStatus; - updateBalanceWriteStatus: WriteContractStatus; - updateBalanceTransactionStatus: GenericContractStatus; - claimSimulateStatus: GenericContractStatus; - claimWriteStatus: WriteContractStatus; - claimTransactionStatus: GenericContractStatus; - skipUpdateBalance: boolean; -}) => { - const stage = useMemo(() => { - if ( - (skipUpdateBalance || - (updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus === 'success' && - updateBalanceTransactionStatus === 'success')) && - claimSimulateStatus === 'success' && - claimWriteStatus === 'success' && - claimTransactionStatus === 'success' - ) { - return CLAIM_REWARDS_STATE.TRANSACTION_CLAIM; - } - - if ( - (skipUpdateBalance || - (updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus === 'success' && - updateBalanceTransactionStatus === 'success')) && - claimSimulateStatus === 'success' && - claimWriteStatus === 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.SIMULATE_CLAIM; - } - - if ( - (skipUpdateBalance || - (updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus === 'success' && - updateBalanceTransactionStatus === 'success')) && - claimSimulateStatus === 'success' && - claimWriteStatus !== 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.WRITE_CLAIM; - } - - if ( - (skipUpdateBalance || - (updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus === 'success' && - updateBalanceTransactionStatus === 'success')) && - claimSimulateStatus !== 'success' && - claimWriteStatus !== 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.SIMULATE_CLAIM; - } - - if ( - updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus === 'success' && - updateBalanceTransactionStatus !== 'success' && - claimSimulateStatus !== 'success' && - claimWriteStatus !== 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.TRANSACTION_UPDATE_BALANCE; - } - - if ( - updateBalanceSimulateStatus === 'success' && - updateBalanceWriteStatus !== 'success' && - updateBalanceTransactionStatus !== 'success' && - claimSimulateStatus !== 'success' && - claimWriteStatus !== 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.WRITE_UPDATE_BALANCE; - } - - if ( - updateBalanceSimulateStatus !== 'success' && - updateBalanceWriteStatus !== 'success' && - updateBalanceTransactionStatus !== 'success' && - claimSimulateStatus !== 'success' && - claimWriteStatus !== 'success' && - claimTransactionStatus !== 'success' - ) { - return CLAIM_REWARDS_STATE.SIMULATE_UPDATE_BALANCE; - } - return CLAIM_REWARDS_STATE.SIMULATE_UPDATE_BALANCE; - }, [ - updateBalanceSimulateStatus, - updateBalanceWriteStatus, - updateBalanceTransactionStatus, - claimSimulateStatus, - claimWriteStatus, - claimTransactionStatus, - skipUpdateBalance, - ]); - - const subStage = useMemo(() => { - switch (stage) { - case CLAIM_REWARDS_STATE.SIMULATE_UPDATE_BALANCE: - return updateBalanceSimulateStatus; - case CLAIM_REWARDS_STATE.WRITE_UPDATE_BALANCE: - return updateBalanceWriteStatus; - case CLAIM_REWARDS_STATE.TRANSACTION_UPDATE_BALANCE: - return updateBalanceTransactionStatus; - case CLAIM_REWARDS_STATE.SIMULATE_CLAIM: - return claimSimulateStatus; - case CLAIM_REWARDS_STATE.WRITE_CLAIM: - return claimWriteStatus; - case CLAIM_REWARDS_STATE.TRANSACTION_CLAIM: - return claimTransactionStatus; - default: - return 'pending'; - } - }, [ - stage, - updateBalanceSimulateStatus, - updateBalanceWriteStatus, - updateBalanceTransactionStatus, - claimSimulateStatus, - claimWriteStatus, - claimTransactionStatus, - ]); - - return { stage, subStage }; -}; +import { + formatAndHandleLocalizedContractErrorMessages, + parseContractStatusToProgressStatus, +} from '@/lib/contracts'; type UseClaimRewardsParams = UseUpdateRewardsBalanceQueryParams; @@ -172,19 +25,16 @@ export default function useClaimRewards({ const [enabled, setEnabled] = useState(false); const [skipUpdateBalance, setSkipUpdateBalance] = useState(false); - const dictionary = useTranslations('modules.claim.stage'); - const dictionaryFee = useTranslations('modules.claim.dialog.alert'); + const stageDictKey = 'modules.claim.stage' as const; + const dictionary = useTranslations(stageDictKey); + const dictionaryGeneral = useTranslations('general'); const { updateRewardsBalance, fee: updateBalanceFee, - gasPrice: updateBalanceGasPrice, - gasAmountEstimate: updateBalanceGasAmountEstimate, estimateContractWriteFee: updateBalanceEstimateContractWriteFee, refetchContractWriteFeeEstimate: updateBalanceRefetchContractWriteFeeEstimate, - estimateFeeStatus: updateBalanceEstimateFeeStatus, - simulateStatus: updateBalanceSimulateStatus, - writeStatus: updateBalanceWriteStatus, + contractCallStatus: updateBalanceContractCallStatus, transactionStatus: updateBalanceTransactionStatus, estimateFeeError: updateBalanceEstimateFeeError, simulateError: updateBalanceSimulateError, @@ -195,29 +45,23 @@ export default function useClaimRewards({ const { claimRewards, fee: claimFee, - gasPrice: claimGasPrice, - gasAmountEstimate: claimGasAmountEstimate, estimateContractWriteFee: claimEstimateContractWriteFee, refetchContractWriteFeeEstimate: claimRefetchContractWriteFeeEstimate, - estimateFeeStatus: claimEstimateFeeStatus, - simulateStatus: claimSimulateStatus, - writeStatus: claimWriteStatus, - transactionStatus: claimTransactionStatus, - estimateFeeError: claimEstimateFeeError, + contractCallStatus: claimContractCallStatus, simulateError: claimSimulateError, writeError: claimWriteError, transactionError: claimTransactionError, } = useClaimRewardsQuery(); - const { stage, subStage } = useClaimRewardsStage({ - updateBalanceSimulateStatus, - updateBalanceWriteStatus, - updateBalanceTransactionStatus, - claimSimulateStatus, - claimWriteStatus, - claimTransactionStatus, - skipUpdateBalance, - }); + const updateRewardsBalanceStatus = useMemo( + () => parseContractStatusToProgressStatus(updateBalanceContractCallStatus), + [updateBalanceContractCallStatus] + ); + + const claimRewardsStatus = useMemo( + () => parseContractStatusToProgressStatus(claimContractCallStatus), + [claimContractCallStatus] + ); const estimateFee = () => { updateBalanceEstimateContractWriteFee(); @@ -236,92 +80,61 @@ export default function useClaimRewards({ } }; + const updateRewardsBalanceErrorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'balance', + dictionary, + dictionaryGeneral, + simulateError: updateBalanceSimulateError, + writeError: updateBalanceWriteError, + transactionError: updateBalanceTransactionError, + }), + [updateBalanceSimulateError, updateBalanceWriteError, updateBalanceTransactionError] + ); + + const claimRewardsErrorMessage = useMemo( + () => + formatAndHandleLocalizedContractErrorMessages({ + parentDictKey: stageDictKey, + errorGroupDictKey: 'claim', + dictionary, + dictionaryGeneral, + simulateError: claimSimulateError, + writeError: claimWriteError, + transactionError: claimTransactionError, + }), + [claimSimulateError, claimWriteError, claimTransactionError] + ); + useEffect(() => { if (enabled && (skipUpdateBalance || updateBalanceTransactionStatus === 'success')) { claimRewards(); } }, [enabled, skipUpdateBalance, updateBalanceTransactionStatus]); - /** - * NOTE: All of these useEffects are required to inform the user of errors via the toaster - */ useEffect(() => { - if (updateBalanceEstimateFeeError) { - // If the gas estimation fails with the RecipientRewardsTooLow error, we can skip the update balance step - // @ts-expect-error -- TODO: Properly type this error - if (updateBalanceEstimateFeeError?.cause?.data?.abiItem?.name === 'RecipientRewardsTooLow') { - setSkipUpdateBalance(true); - } else { - toast.handleError(updateBalanceEstimateFeeError); - toast.error(dictionaryFee('gasFetchFailedUpdateBalance')); - } + // If the gas estimation fails with the RecipientRewardsTooLow error, we can skip the update balance step + if ( + updateBalanceEstimateFeeError && + getContractErrorName(updateBalanceEstimateFeeError) === 'RecipientRewardsTooLow' + ) { + setSkipUpdateBalance(true); } }, [updateBalanceEstimateFeeError]); - useEffect(() => { - if (updateBalanceSimulateError) { - toast.handleError(updateBalanceSimulateError); - toast.error(dictionary('updateBalance.simulate.errorTooltip')); - } - }, [updateBalanceSimulateError]); - - useEffect(() => { - if (updateBalanceWriteError) { - toast.handleError(updateBalanceWriteError); - toast.error(dictionary('updateBalance.write.errorTooltip')); - } - }, [updateBalanceWriteError]); - - useEffect(() => { - if (updateBalanceTransactionError) { - toast.handleError(updateBalanceTransactionError); - toast.error(dictionary('updateBalance.transaction.errorTooltip')); - } - }, [updateBalanceTransactionError]); - - useEffect(() => { - if (claimEstimateFeeError) { - toast.handleError(claimEstimateFeeError); - toast.error(dictionaryFee('gasFetchFailedClaimRewards')); - } - }, [claimEstimateFeeError]); - - useEffect(() => { - if (claimSimulateError) { - toast.handleError(claimSimulateError); - toast.error(dictionary('claimRewards.simulate.errorTooltip')); - } - }, [claimSimulateError]); - - useEffect(() => { - if (claimWriteError) { - toast.handleError(claimWriteError); - toast.error(dictionary('claimRewards.write.errorTooltip')); - } - }, [claimWriteError]); - - useEffect(() => { - if (claimTransactionError) { - toast.handleError(claimTransactionError); - toast.error(dictionary('claimRewards.transaction.errorTooltip')); - } - }, [claimTransactionError]); - return { updateBalanceAndClaimRewards, - estimateFee, refetchFeeEstimate, - updateBalanceFee, - updateBalanceGasPrice, - updateBalanceGasAmountEstimate, claimFee, - claimGasPrice, - claimGasAmountEstimate, - stage, - subStage, + updateBalanceFee, + estimateFee, + updateRewardsBalanceStatus, + claimRewardsStatus, enabled, - updateBalanceEstimateFeeStatus, - claimEstimateFeeStatus, skipUpdateBalance, + updateRewardsBalanceErrorMessage, + claimRewardsErrorMessage, }; } From b78dc64eb252dcc96fc78b84bdab0a94668cf48e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:56:32 +1000 Subject: [PATCH 19/26] fix: action module header margin --- apps/staking/components/ActionModule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staking/components/ActionModule.tsx b/apps/staking/components/ActionModule.tsx index e372cec6..5a939bba 100644 --- a/apps/staking/components/ActionModule.tsx +++ b/apps/staking/components/ActionModule.tsx @@ -37,7 +37,7 @@ export default function ActionModule({ {title ? ( <> {title} - {headerAction} +
{headerAction}
) : null} From e57f3a73857c2cfac59e2ee5b62d1928d8666965 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:56:42 +1000 Subject: [PATCH 20/26] feat: add contract actions to dev sheet --- apps/staking/components/DevSheet.tsx | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/apps/staking/components/DevSheet.tsx b/apps/staking/components/DevSheet.tsx index a1843491..3a069687 100644 --- a/apps/staking/components/DevSheet.tsx +++ b/apps/staking/components/DevSheet.tsx @@ -30,6 +30,15 @@ import { useSetFeatureFlag, } from '@/lib/feature-flags-client'; import { CopyToClipboardButton } from '@session/ui/components/CopyToClipboardButton'; +import { + formatSENTBigInt, + useAllowanceQuery, + useProxyApproval, +} from '@session/contracts/hooks/SENT'; +import { addresses, SENT_DECIMALS } from '@session/contracts'; +import { LoadingText } from '@session/ui/components/loading-text'; +import { Button } from '@session/ui/ui/button'; +import { Input } from '@session/ui/ui/input'; export function DevSheet({ buildInfo }: { buildInfo: BuildInfo }) { const [isOpen, setIsOpen] = useState(false); @@ -132,6 +141,8 @@ export function DevSheet({ buildInfo }: { buildInfo: BuildInfo }) { ))} + Contract Actions + @@ -188,3 +199,62 @@ function FeatureFlagToggle({ ); } + +function ContractActions() { + const [value, setValue] = useState('0'); + const serviceNodeRewardsAddress = addresses.ServiceNodeRewards.testnet; + + const tokenAmount = useMemo(() => BigInt(value) * BigInt(10 ** SENT_DECIMALS), [value]); + + const { + allowance, + getAllowance, + refetch, + status: allowanceStatus, + } = useAllowanceQuery({ + contractAddress: serviceNodeRewardsAddress, + }); + + const { approveWrite, resetApprove, status } = useProxyApproval({ + contractAddress: serviceNodeRewardsAddress, + tokenAmount, + }); + + const handleClick = () => { + if (status !== 'idle') { + resetApprove(); + } + approveWrite(); + }; + + useEffect(() => { + if (!allowance) getAllowance(); + }, [allowance]); + + useEffect(() => { + if (status === 'success') refetch().then(() => getAllowance()); + }, [status]); + + return ( + <> + + + {'Allowance:'} + + {allowanceStatus === 'success' ? formatSENTBigInt(allowance) : } + + + + setValue(e.target.value)} /> + + + ); +} From 497927ca787c40d4aaa34b3d84199f2e3983135b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 16 Sep 2024 16:57:01 +1000 Subject: [PATCH 21/26] fix: not found registration finding already registered nodes --- .../app/register/[nodeId]/not-found.tsx | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/apps/staking/app/register/[nodeId]/not-found.tsx b/apps/staking/app/register/[nodeId]/not-found.tsx index 6cf76b5a..0142659e 100644 --- a/apps/staking/app/register/[nodeId]/not-found.tsx +++ b/apps/staking/app/register/[nodeId]/not-found.tsx @@ -4,23 +4,37 @@ import { useTranslations } from 'next-intl'; import { getOpenNodes } from '@/lib/queries/getOpenNodes'; import ActionModule from '@/components/ActionModule'; import { useMemo } from 'react'; -import { useStakingBackendSuspenseQuery } from '@/lib/sent-staking-backend-client'; +import { + useStakingBackendQueryWithParams, + useStakingBackendSuspenseQuery, +} from '@/lib/sent-staking-backend-client'; import { NodeStakingForm } from '@/app/stake/[nodeId]/NodeStaking'; import { usePathname } from 'next/navigation'; -import { type StakedNode, StakedNodeCard } from '@/components/StakedNodeCard'; -import { getNode } from '@/lib/queries/getNode'; -import { useQuery } from '@tanstack/react-query'; +import { StakedNode, StakedNodeCard } from '@/components/StakedNodeCard'; import { areHexesEqual } from '@session/util/string'; +import { getStakedNodes } from '@/lib/queries/getStakedNodes'; +import { useWallet } from '@session/wallet/hooks/wallet-hooks'; +import { parseSessionNodeData } from '@/app/mystakes/modules/StakedNodesModule'; +import { useQuery } from '@tanstack/react-query'; +import { getNode } from '@/lib/queries/getNode'; export default function NotFound() { const registerDictionary = useTranslations('actionModules.register'); + const { address } = useWallet(); const pathname = usePathname(); - const nodeId = pathname.split('/').at(-2); + const nodeId = pathname.split('/').at(-1); const { data: openData } = useStakingBackendSuspenseQuery(getOpenNodes); - const { data: runningNode } = useQuery({ + const { data: stakedNodesData } = useStakingBackendQueryWithParams( + getStakedNodes, + { + address: address!, + }, + { enabled: !!address } + ); + const { data: runningGlobalNode } = useQuery({ queryKey: ['getNode', nodeId], queryFn: () => getNode({ address: nodeId! }), enabled: Boolean(nodeId), @@ -30,18 +44,31 @@ export default function NotFound() { return openData?.nodes?.find((node) => areHexesEqual(node.service_node_pubkey, nodeId)); }, [openData, nodeId]); - const nodeAlreadyRunning = runningNode && 'state' in runningNode && runningNode.state; + const nodeStakedTo = useMemo(() => { + return stakedNodesData?.nodes?.find((node) => areHexesEqual(node.service_node_pubkey, nodeId)); + }, [stakedNodesData, nodeId]); + + const nodeRunningElsewhere = + runningGlobalNode && 'state' in runningGlobalNode && runningGlobalNode.state; return ( {registerDictionary('notFound.description')}
- {nodeAlreadyRunning ? ( + {nodeStakedTo ? ( + <> + + {registerDictionary.rich('notFound.foundRunningNode')} + + +
+ + ) : nodeRunningElsewhere ? ( <> - {registerDictionary('notFound.foundRunningNode')} + {registerDictionary('notFound.foundRunningNodeOtherOperator')} - +
) : null} From eca549ba369fde7d3e79a37ed3d5f20de5e5fbd1 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 17 Sep 2024 09:53:30 +1000 Subject: [PATCH 22/26] fix: smart contract read hook not updating properly --- apps/staking/components/DevSheet.tsx | 15 ++++---- packages/contracts/hooks/RewardRatePool.tsx | 10 +----- packages/contracts/hooks/SENT.tsx | 29 ++++----------- .../contracts/hooks/ServiceNodeRewards.tsx | 10 +----- .../contracts/hooks/useContractReadQuery.tsx | 35 ++++++------------- packages/wallet/components/WalletButton.tsx | 11 ++++-- packages/wallet/hooks/wallet-hooks.tsx | 2 ++ 7 files changed, 37 insertions(+), 75 deletions(-) diff --git a/apps/staking/components/DevSheet.tsx b/apps/staking/components/DevSheet.tsx index 3a069687..4af8cf44 100644 --- a/apps/staking/components/DevSheet.tsx +++ b/apps/staking/components/DevSheet.tsx @@ -208,7 +208,6 @@ function ContractActions() { const { allowance, - getAllowance, refetch, status: allowanceStatus, } = useAllowanceQuery({ @@ -228,11 +227,7 @@ function ContractActions() { }; useEffect(() => { - if (!allowance) getAllowance(); - }, [allowance]); - - useEffect(() => { - if (status === 'success') refetch().then(() => getAllowance()); + if (status === 'success') refetch(); }, [status]); return ( @@ -253,7 +248,13 @@ function ContractActions() { rounded="md" disabled={status === 'pending' || tokenAmount === allowance} > - {tokenAmount > BigInt(0) ? 'Set Allowance' : 'Reset Allowance'} + {status === 'pending' ? ( + + ) : tokenAmount > BigInt(0) ? ( + 'Set Allowance' + ) : ( + 'Reset Allowance' + )} ); diff --git a/packages/contracts/hooks/RewardRatePool.tsx b/packages/contracts/hooks/RewardRatePool.tsx index bff1db41..b6ac96e4 100644 --- a/packages/contracts/hooks/RewardRatePool.tsx +++ b/packages/contracts/hooks/RewardRatePool.tsx @@ -8,28 +8,20 @@ import { useChain } from './useChain'; type RewardRate = ReadContractData; export type RewardRateQuery = ContractReadQueryProps & { - /** Get the reward rate */ - getRewardRate: () => void; /** The reward rate */ rewardRate: RewardRate; }; export function useRewardRateQuery(): RewardRateQuery { const chain = useChain(); - const { - data: rewardRate, - readContract, - ...rest - } = useContractReadQuery({ + const { data: rewardRate, ...rest } = useContractReadQuery({ contract: 'RewardRatePool', functionName: 'rewardRate', - startEnabled: true, chain, }); return { rewardRate, - getRewardRate: readContract, ...rest, }; } diff --git a/packages/contracts/hooks/SENT.tsx b/packages/contracts/hooks/SENT.tsx index 868b388b..a752a931 100644 --- a/packages/contracts/hooks/SENT.tsx +++ b/packages/contracts/hooks/SENT.tsx @@ -27,8 +27,6 @@ export const formatSENTNumber = (value?: number, decimals?: number, hideSymbol?: type SENTBalance = ReadContractData; export type SENTBalanceQuery = ContractReadQueryProps & { - /** Get the session token balance */ - getBalance: () => void; /** The session token balance */ balance: SENTBalance; }; @@ -41,21 +39,17 @@ export function useSENTBalanceQuery({ overrideChain?: CHAIN; }): SENTBalanceQuery { const chain = useChain(); - const { - data: balance, - readContract, - ...rest - } = useContractReadQuery({ + + const { data: balance, ...rest } = useContractReadQuery({ contract: 'SENT', functionName: 'balanceOf', - defaultArgs: [address!], - startEnabled: !!address, + args: [address!], + enabled: !!address, chain: overrideChain ?? chain, }); return { balance, - getBalance: readContract, ...rest, }; } @@ -63,8 +57,6 @@ export function useSENTBalanceQuery({ type SENTAllowance = ReadContractData; export type SENTAllowanceQuery = ContractReadQueryProps & { - /** Get the session token allowance */ - getAllowance: () => void; /** The session token allowance for a contract */ allowance: SENTAllowance; }; @@ -76,20 +68,16 @@ export function useAllowanceQuery({ }): SENTAllowanceQuery { const { address } = useAccount(); const chain = useChain(); - const { - data: allowance, - readContract, - ...rest - } = useContractReadQuery({ + const { data: allowance, ...rest } = useContractReadQuery({ contract: 'SENT', functionName: 'allowance', - defaultArgs: [address!, contractAddress], + args: [address!, contractAddress], + enabled: !!address, chain, }); return { allowance, - getAllowance: readContract, ...rest, }; } @@ -120,7 +108,6 @@ export function useProxyApproval({ const { address } = useAccount(); const { allowance, - getAllowance, status: readStatusRaw, refetch: refetchRaw, } = useAllowanceQuery({ @@ -154,8 +141,6 @@ export function useProxyApproval({ const approve = () => { if (allowance) { void refetchAllowance(); - } else { - getAllowance(); } }; diff --git a/packages/contracts/hooks/ServiceNodeRewards.tsx b/packages/contracts/hooks/ServiceNodeRewards.tsx index 567b429c..bcec759e 100644 --- a/packages/contracts/hooks/ServiceNodeRewards.tsx +++ b/packages/contracts/hooks/ServiceNodeRewards.tsx @@ -69,28 +69,20 @@ export function useUpdateRewardsBalanceQuery({ } export type TotalNodesQuery = ContractReadQueryProps & { - /** Update rewards balance */ - getTotalNodes: () => void; /** The total number of nodes */ totalNodes: ReadContractData; }; export function useTotalNodesQuery(): TotalNodesQuery { const chain = useChain(); - const { - data: totalNodes, - readContract, - ...rest - } = useContractReadQuery({ + const { data: totalNodes, ...rest } = useContractReadQuery({ contract: 'ServiceNodeRewards', functionName: 'totalNodes', - startEnabled: true, chain, }); return { totalNodes, - getTotalNodes: readContract, ...rest, }; } diff --git a/packages/contracts/hooks/useContractReadQuery.tsx b/packages/contracts/hooks/useContractReadQuery.tsx index c25b2485..9f1c80c5 100644 --- a/packages/contracts/hooks/useContractReadQuery.tsx +++ b/packages/contracts/hooks/useContractReadQuery.tsx @@ -1,5 +1,5 @@ import { QueryObserverResult, RefetchOptions } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import type { Abi, ContractFunctionArgs, ContractFunctionName, ReadContractErrorType } from 'viem'; import { useReadContract } from 'wagmi'; import { ReadContractData } from 'wagmi/query'; @@ -8,8 +8,6 @@ import { CHAIN, chains } from '../chains'; import { addresses, type ContractName } from '../constants'; import type { GenericContractStatus } from './useContractWriteQuery'; -export type ReadContractFunction = (args?: Args) => void; - export type ContractReadQueryProps = { /** The status of the read contract */ status: GenericContractStatus; @@ -21,16 +19,14 @@ export type ContractReadQueryProps = { ) => Promise>; }; -export type UseContractRead = ContractReadQueryProps & { - /** Read the contract */ - readContract: ReadContractFunction; +export type UseContractRead = ContractReadQueryProps & { /** The data from the contract */ data: Data; }; export type ContractReadQueryFetchOptions = { - /** Set startEnabled to true to enable automatic fetching when the query mounts or changes query keys. To manually fetch the query, use the readContract method returned from the useContractReadQuery instance. Defaults to false. */ - startEnabled?: boolean; + /** Set enabled to true to enable automatic fetching when the query mounts or changes query keys. To manually fetch the query, use the readContract method returned from the useContractReadQuery instance. Defaults to false. */ + enabled?: boolean; /** Chain the contract is on */ chain: CHAIN; }; @@ -44,39 +40,28 @@ export function useContractReadQuery< >({ contract, functionName, - startEnabled = false, - defaultArgs, + enabled, + args, chain, }: { contract: T; - defaultArgs?: Args; + args?: Args; functionName: FName; -} & ContractReadQueryFetchOptions): UseContractRead { - const [readEnabled, setReadEnabled] = useState(startEnabled); - const [contractArgs, setContractArgs] = useState(defaultArgs); - +} & ContractReadQueryFetchOptions): UseContractRead { const abi = useMemo(() => Contracts[contract], [contract]); const address = useMemo(() => addresses[contract][chain], [contract, chain]); const { data, status, refetch, error } = useReadContract({ - query: { - enabled: readEnabled, - }, address: address, abi: abi as Abi, functionName: functionName, - args: contractArgs as ContractFunctionArgs, + args: args as ContractFunctionArgs, chainId: chains[chain].id, + query: { enabled }, }); - const readContract: ReadContractFunction = (args) => { - if (args) setContractArgs(args); - setReadEnabled(true); - }; - return { data: data as Data, - readContract, status, refetch, error, diff --git a/packages/wallet/components/WalletButton.tsx b/packages/wallet/components/WalletButton.tsx index ae3e79cb..38f5c3ce 100644 --- a/packages/wallet/components/WalletButton.tsx +++ b/packages/wallet/components/WalletButton.tsx @@ -1,13 +1,13 @@ -import { SENT_DECIMALS, SENT_SYMBOL } from '@session/contracts'; +import { SENT_SYMBOL } from '@session/contracts'; import { Button } from '@session/ui/components/ui/button'; import { SessionTokenIcon } from '@session/ui/icons/SessionTokenIcon'; import { cn } from '@session/ui/lib/utils'; -import { formatBigIntTokenValue } from '@session/util/maths'; import { collapseString } from '@session/util/string'; import { useMemo } from 'react'; import { ButtonDataTestId } from '../testing/data-test-ids'; import { ConnectedWalletAvatar } from './WalletAvatar'; import { WalletButtonProps } from './WalletModalButton'; +import { formatSENTBigInt } from '@session/contracts/hooks/SENT'; export function WalletButton({ labels, @@ -31,6 +31,11 @@ export function WalletButton({ [ensName, arbName, address, fallbackName] ); + const formattedBalance = useMemo( + () => (tokenBalance ? formatSENTBigInt(tokenBalance) : `0 ${SENT_SYMBOL}`), + [tokenBalance] + ); + return (
) : null}
Date: Tue, 17 Sep 2024 16:52:09 +1000 Subject: [PATCH 23/26] fix: minor alignment issues --- apps/staking/app/mystakes/modules/BalanceModule.tsx | 2 +- apps/staking/components/ActionModule.tsx | 2 +- packages/ui/components/motion/progress.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/staking/app/mystakes/modules/BalanceModule.tsx b/apps/staking/app/mystakes/modules/BalanceModule.tsx index 6a0fda7b..031beb9d 100644 --- a/apps/staking/app/mystakes/modules/BalanceModule.tsx +++ b/apps/staking/app/mystakes/modules/BalanceModule.tsx @@ -91,7 +91,7 @@ export default function BalanceModule() { refetch, }} style={{ - fontSize: getVariableFontSizeForLargeModule(totalStakedAmount?.length ?? 5), + fontSize: getVariableFontSizeForLargeModule(totalStakedAmount?.length ?? 6), }} > {totalStakedAmount} diff --git a/apps/staking/components/ActionModule.tsx b/apps/staking/components/ActionModule.tsx index 5a939bba..02c4b64d 100644 --- a/apps/staking/components/ActionModule.tsx +++ b/apps/staking/components/ActionModule.tsx @@ -37,7 +37,7 @@ export default function ActionModule({ {title ? ( <> {title} -
{headerAction}
+
{headerAction}
) : null} diff --git a/packages/ui/components/motion/progress.tsx b/packages/ui/components/motion/progress.tsx index 25e07e38..5262f079 100644 --- a/packages/ui/components/motion/progress.tsx +++ b/packages/ui/components/motion/progress.tsx @@ -1,5 +1,4 @@ import { forwardRef, type HTMLAttributes, useMemo } from 'react'; -import { clsx } from 'clsx'; import { motion } from 'framer-motion'; import { Circle } from './shapes/circle'; import { cn } from '../../lib/utils'; @@ -80,6 +79,7 @@ function ProgressStep({ return (
( ({ steps, className, ...props }, ref) => { return ( -
+
{steps.map(({ text, status }, i) => ( Date: Tue, 17 Sep 2024 16:53:05 +1000 Subject: [PATCH 24/26] chore: create indicator colours in tailwind --- .../ui/components/motion/shapes/circle.tsx | 34 +++++++++---------- packages/ui/styles/global.css | 7 ++++ packages/ui/tailwind.config.ts | 8 +++++ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/ui/components/motion/shapes/circle.tsx b/packages/ui/components/motion/shapes/circle.tsx index a24d9d8a..c96b672d 100644 --- a/packages/ui/components/motion/shapes/circle.tsx +++ b/packages/ui/components/motion/shapes/circle.tsx @@ -6,28 +6,28 @@ import { cn } from '../../../lib/utils'; const circleVariants = cva('', { variants: { variant: { - black: 'fill-black', - grey: 'fill-[#4A4A4A]', - green: 'fill-[#00F782]', - blue: 'fill-[#00A3F7]', - yellow: 'fill-[#F7DE00]', - red: 'fill-red-500', + black: 'fill-indicator-black', + grey: 'fill-indicator-grey', + green: 'fill-indicator-green', + blue: 'fill-indicator-blue', + yellow: 'fill-indicator-yellow', + red: 'fill-indicator-red', }, strokeVariant: { - black: 'stroke-black', - grey: 'stroke-[#4A4A4A]', - green: 'stroke-[#00F782]', - blue: 'stroke-[#00A3F7]', - yellow: 'stroke-[#F7DE00]', - red: 'stroke-red-500', + black: 'stroke-indicator-black', + grey: 'stroke-indicator-grey', + green: 'stroke-indicator-green', + blue: 'stroke-indicator-blue', + yellow: 'stroke-indicator-yellow', + red: 'stroke-indicator-red', }, glow: { black: '', - grey: 'drop-shadow-[0_0_8px_#4A4A4A] glow-grey', - green: 'drop-shadow-[0_0_8px_#00F782] glow', - blue: 'drop-shadow-[0_0_8px_#00A3F7] glow-blue', - yellow: 'drop-shadow-[0_0_8px_#F7DE00] glow-yellow', - red: 'drop-shadow-[0_0_8px_F70000] glow-red', + grey: 'drop-shadow-[0_0_8px_var(--indicator-grey)] glow-grey', + green: 'drop-shadow-[0_0_8px_var(--indicator-green)] glow', + blue: 'drop-shadow-[0_0_8px_var(--indicator-blue)] glow-blue', + yellow: 'drop-shadow-[0_0_8px_var(--indicator-yellow)] glow-yellow', + red: 'drop-shadow-[0_0_8px_var(--indicator-red)] glow-red', }, partial: { '100': '', diff --git a/packages/ui/styles/global.css b/packages/ui/styles/global.css index 7f130d5c..37b38352 100644 --- a/packages/ui/styles/global.css +++ b/packages/ui/styles/global.css @@ -82,6 +82,13 @@ --header-vertical-padding: 2rem; --header-displacement: calc(var(--header-height) + var(--header-vertical-padding) * 2); --screen-without-header: calc(100vh - var(--header-displacement)); + + --indicator-black: var(--session-black); + --indicator-grey: rgb(74, 74, 74); + --indicator-green: var(--session-green); + --indicator-blue: rgb(0, 163, 247); + --indicator-yellow: rgb(247, 222, 0); + --indicator-red: rgb(239 68 68); } } diff --git a/packages/ui/tailwind.config.ts b/packages/ui/tailwind.config.ts index ea56c154..bf32f966 100644 --- a/packages/ui/tailwind.config.ts +++ b/packages/ui/tailwind.config.ts @@ -84,6 +84,14 @@ export default { 900: '#043D22', 950: '#022314', }, + indicator: { + black: 'var(--indicator-black)', + grey: 'var(--indicator-grey)', + green: 'var(--indicator-green)', + blue: 'var(--indicator-blue)', + yellow: 'var(--indicator-yellow)', + red: 'var(--indicator-red)', + }, black: 'var(--session-black)', border: 'hsl(var(--border))', input: 'hsl(var(--input))', From ff5cb639b2b5a707545244d255386d5f20edaf5e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 17 Sep 2024 16:53:24 +1000 Subject: [PATCH 25/26] chore: rename NodeActionModuleInfo --- .../{NodeInfoActionModuleBody.tsx => NodeActionModuleInfo.tsx} | 0 apps/staking/components/StakedNode/NodeExitButtonDialog.tsx | 2 +- apps/staking/components/StakedNode/NodeRequestExitButton.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename apps/staking/components/StakedNode/{NodeInfoActionModuleBody.tsx => NodeActionModuleInfo.tsx} (100%) diff --git a/apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx b/apps/staking/components/StakedNode/NodeActionModuleInfo.tsx similarity index 100% rename from apps/staking/components/StakedNode/NodeInfoActionModuleBody.tsx rename to apps/staking/components/StakedNode/NodeActionModuleInfo.tsx diff --git a/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx b/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx index 46b9bd6b..76fa0aae 100644 --- a/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx +++ b/apps/staking/components/StakedNode/NodeExitButtonDialog.tsx @@ -22,7 +22,7 @@ import { Button } from '@session/ui/ui/button'; import { NodeExitButton } from '@/components/StakedNode/NodeExitButton'; import { useStakingBackendQueryWithParams } from '@/lib/sent-staking-backend-client'; import { getNodeExitSignatures } from '@/lib/queries/getNodeExitSignatures'; -import NodeActionModuleInfo from '@/components/StakedNode/NodeInfoActionModuleBody'; +import NodeActionModuleInfo from '@/components/StakedNode/NodeActionModuleInfo'; import { SENT_SYMBOL } from '@session/contracts'; import { Progress, PROGRESS_STATUS } from '@session/ui/motion/progress'; import useExitNode from '@/hooks/useExitNode'; diff --git a/apps/staking/components/StakedNode/NodeRequestExitButton.tsx b/apps/staking/components/StakedNode/NodeRequestExitButton.tsx index 578b64cd..a1846f9c 100644 --- a/apps/staking/components/StakedNode/NodeRequestExitButton.tsx +++ b/apps/staking/components/StakedNode/NodeRequestExitButton.tsx @@ -26,7 +26,7 @@ import { Social } from '@session/ui/components/SocialLinkList'; import { ChevronsDownIcon } from '@session/ui/icons/ChevronsDownIcon'; import { Progress, PROGRESS_STATUS } from '@session/ui/motion/progress'; import useRequestNodeExit from '@/hooks/useRequestNodeExit'; -import NodeActionModuleInfo from '@/components/StakedNode/NodeInfoActionModuleBody'; +import NodeActionModuleInfo from '@/components/StakedNode/NodeActionModuleInfo'; import { SENT_SYMBOL } from '@session/contracts'; enum EXIT_REQUEST_STATE { From 0972b3622303166eff34a184264b251a9ce5fd8c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 17 Sep 2024 16:59:02 +1000 Subject: [PATCH 26/26] fix: add proper types to circle --- .../ui/components/motion/shapes/circle.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/ui/components/motion/shapes/circle.tsx b/packages/ui/components/motion/shapes/circle.tsx index c96b672d..350fa5e1 100644 --- a/packages/ui/components/motion/shapes/circle.tsx +++ b/packages/ui/components/motion/shapes/circle.tsx @@ -1,4 +1,11 @@ -import { motion, type MotionStyle, Variants } from 'framer-motion'; +import { + AnimationControls, + motion, + type MotionStyle, + TargetAndTransition, + VariantLabels, + Variants, +} from 'framer-motion'; import { forwardRef } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '../../../lib/utils'; @@ -51,13 +58,27 @@ type CircleProps = CircleVariantProps & { strokeWidth?: number; className?: string; variants?: Variants; - animate?: any; + animate?: AnimationControls | TargetAndTransition | VariantLabels; style?: MotionStyle; }; export const Circle = forwardRef( ( - { cx, cy, r, strokeWidth, className, style, variant, strokeVariant, partial, glow, ...props }, + { + cx, + cy, + r, + strokeWidth, + className, + style, + variant, + strokeVariant, + partial, + glow, + animate, + variants, + ...props + }, ref ) => ( ( className={cn(circleVariants({ variant, strokeVariant, partial, glow, className }))} style={style} ref={ref} + variants={variants} + animate={animate} {...props} /> )