From 5515b729f0751478f06595bafdbab59249ee12d3 Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:17:06 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Implemented=20change=20fee=20reciever?= =?UTF-8?q?=20in=20edit=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dapp/app/layout.tsx | 2 +- apps/dapp/app/page.tsx | 10 +- .../components/DeployVault/VaultPreview.tsx | 46 ++-- .../InteractWithVault/EditVault.tsx | 240 ++++++++++++++++++ .../components/ManageVaults/InspectVault.tsx | 13 +- .../components/ManageVaults/ManageVaults.tsx | 18 +- apps/dapp/src/components/ui/button.tsx | 40 +++ apps/dapp/src/contexts/index.ts | 7 +- apps/dapp/src/hooks/useVault.ts | 1 + apps/dapp/src/providers/modal-provider.tsx | 5 + .../src/store/lib/features/walletStore.ts | 12 +- 11 files changed, 356 insertions(+), 38 deletions(-) create mode 100644 apps/dapp/src/components/InteractWithVault/EditVault.tsx create mode 100644 apps/dapp/src/components/ui/button.tsx diff --git a/apps/dapp/app/layout.tsx b/apps/dapp/app/layout.tsx index 7ef5162d..81704878 100644 --- a/apps/dapp/app/layout.tsx +++ b/apps/dapp/app/layout.tsx @@ -6,7 +6,7 @@ export default function RootLayout({ children: ReactNode, }) { return ( - + {children} diff --git a/apps/dapp/app/page.tsx b/apps/dapp/app/page.tsx index 5e0585e9..e011fb93 100644 --- a/apps/dapp/app/page.tsx +++ b/apps/dapp/app/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { Container } from '@chakra-ui/react' +import { Container, HStack } from '@chakra-ui/react' import { useSorobanReact } from '@soroban-react/core' import ManageVaults from '@/components/ManageVaults/ManageVaults'; import { @@ -13,10 +13,8 @@ ChartJS.register(ArcElement); export default function Home() { const { address } = useSorobanReact() return ( - - - - - + + + ); } diff --git a/apps/dapp/src/components/DeployVault/VaultPreview.tsx b/apps/dapp/src/components/DeployVault/VaultPreview.tsx index c0cbea33..d1927875 100644 --- a/apps/dapp/src/components/DeployVault/VaultPreview.tsx +++ b/apps/dapp/src/components/DeployVault/VaultPreview.tsx @@ -155,6 +155,29 @@ interface VaultPreviewProps { setFormControl: (args: FormControlInterface) => any; } + +export const dropdownData = { + strategies: { + title: 'Strategies', + description: 'A strategy is a set of steps to be followed to execute an investment in one or several protocols.', + href: 'https://docs.defindex.io/whitepaper/10-whitepaper/01-introduction#core-concepts' + }, + manager: { + title: 'Manager', + description: 'The Manager can rebalance the Vault, emergency withdraw and invest IDLE funds in strategies.', + href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#management' + }, + emergencyManager: { + title: 'Emergency manager', + description: 'The Emergency Manager has the authority to withdraw assets from the DeFindex in case of an emergency.', + href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#emergency-management' + }, + feeReceiver: { + title: 'Fee receiver', + description: ' Fee Receiver could be the manager using the same address, or it could be a different entity such as a streaming contract, a DAO, or another party.', + href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#fee-collection' + } +} export const VaultPreview: React.FC = ({ data, accordionValue, setAccordionValue, formControl, setFormControl }) => { const dispatch = useAppDispatch() @@ -248,28 +271,7 @@ export const VaultPreview: React.FC = ({ data, accordionValue dispatch(setVaultShare(input * 100)) } - const dropdownData = { - strategies: { - title: 'Strategies', - description: 'A strategy is a set of steps to be followed to execute an investment in one or several protocols.', - href: 'https://docs.defindex.io/whitepaper/10-whitepaper/01-introduction#core-concepts' - }, - manager: { - title: 'Manager', - description: 'The Manager can rebalance the Vault, emergency withdraw and invest IDLE funds in strategies.', - href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#management' - }, - emergencyManager: { - title: 'Emergency manager', - description: 'The Emergency Manager has the authority to withdraw assets from the DeFindex in case of an emergency.', - href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#emergency-management' - }, - feeReceiver: { - title: 'Fee receiver', - description: ' Fee Receiver could be the manager using the same address, or it could be a different entity such as a streaming contract, a DAO, or another party.', - href: 'https://docs.defindex.io/whitepaper/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract#fee-collection' - } - } + return ( <> setAccordionValue(e.value)}> diff --git a/apps/dapp/src/components/InteractWithVault/EditVault.tsx b/apps/dapp/src/components/InteractWithVault/EditVault.tsx new file mode 100644 index 00000000..2be0c278 --- /dev/null +++ b/apps/dapp/src/components/InteractWithVault/EditVault.tsx @@ -0,0 +1,240 @@ +import React, { useContext, useEffect, useState } from 'react' +import { Address, xdr } from '@stellar/stellar-sdk' +import { useSorobanReact } from '@soroban-react/core' + +import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks' +import { setVaultFeeReceiver } from '@/store/lib/features/walletStore' +import { setFeeReceiver } from '@/store/lib/features/vaultStore' + +import { ModalContext } from '@/contexts' +import { VaultMethod, useVaultCallback, useVault } from '@/hooks/useVault' +import { isValidAddress } from '@/helpers/address' + +import { DialogBody, DialogContent, DialogHeader } from '../ui/dialog' +import { InputGroup } from '../ui/input-group' +import { + Input, + Text, + Stack, + HStack, + Fieldset, + Link, + IconButton, +} from '@chakra-ui/react' +import { InfoTip } from '../ui/toggle-tip' +import { Tooltip } from '../ui/tooltip' +import { FaRegPaste } from 'react-icons/fa6' +import { dropdownData } from '../DeployVault/VaultPreview' +import { Button } from '../ui/button' + +const CustomInputField = ({ + label, + value, + onChange, + handleClick, + placeholder, + invalid, + description, + href, +}: { + label: string, + value: string, + onChange: (e: any) => void, + handleClick: (address: string) => void, + placeholder: string, + invalid: boolean, + description?: string, + href?: string, +}) => { + const { address } = useSorobanReact() + return ( + + {label} + + {description} + + Learn more. + + + + } /> + + + + handleClick(address!)} + > + + + + } + > + + + + A valid Stellar / Soroban address is required. + + ) +} + +export const EditVaultModal = () => { + const selectedVault = useAppSelector(state => state.wallet.vaults.selectedVault) + const vaultMethod = selectedVault?.method + + const { address } = useSorobanReact(); + const vaultCB = useVaultCallback() + const vault = useVault() + const dispatch = useAppDispatch() + const { transactionStatusModal: statusModal, interactWithVaultModal: interactModal, inspectVaultModal: inspectModal } = useContext(ModalContext) + const [formControl, setFormControl] = useState({ + feeReceiver: { + value: selectedVault?.feeReceiver ?? '', + isValid: false, + needsUpdate: false, + isLoading: false, + } + }) + + + + + const handleFeeReceiverChange = (input: string) => { + const isValid = isValidAddress(input) + while (!isValid) { + setFormControl({ + ...formControl, + feeReceiver: { + ...formControl.feeReceiver, + value: input, + isValid: false, + } + }) + dispatch(setFeeReceiver('')) + return + } + setFormControl({ + ...formControl, + feeReceiver: { + ...formControl.feeReceiver, + value: input, + isValid: true, + } + }) + }; + + useEffect(() => { + setFormControl({ + ...formControl, + feeReceiver: { + isValid: isValidAddress(selectedVault?.feeReceiver ?? ''), + value: selectedVault?.feeReceiver ?? '', + needsUpdate: false, + isLoading: false, + } + }) + }, [selectedVault]) + enum Values { + FEERECIEVER = 'feeReceiver', + MANAGER = 'manager', + EMERGENCYMANAGER = 'emergencyManager' + } + + const updateValue = async (value: Values) => { + if (!address || !selectedVault) return; + let result: any; + if (value === Values.FEERECIEVER) { + setFormControl({ feeReceiver: { ...formControl.feeReceiver, isLoading: true } }) + statusModal.initModal() + console.log('Updating fee receiver') + const caller = new Address(address); + const feeReceiver = new Address(formControl.feeReceiver.value); + const createDefindexParams: xdr.ScVal[] = [ + caller.toScVal(), + feeReceiver.toScVal(), + ]; + try { + result = await vaultCB(VaultMethod.SETFEERECIEVER, selectedVault.address, createDefindexParams, true).then((res) => { + console.log(res) + statusModal.handleSuccess(res.txHash) + dispatch(setVaultFeeReceiver(formControl.feeReceiver.value)) + }) + } catch (error: any) { + console.error('Error:', error) + statusModal.handleError(error.toString()) + } finally { + setFormControl({ feeReceiver: { ...formControl.feeReceiver, isLoading: false } }) + } + + }; + } + + useEffect(() => { + if (!selectedVault?.feeReceiver) return + if (formControl.feeReceiver.value !== selectedVault.feeReceiver && formControl.feeReceiver.isValid) { + setFormControl({ + ...formControl, + feeReceiver: { + ...formControl.feeReceiver, + needsUpdate: true, + } + }) + } else if (formControl.feeReceiver.value === selectedVault.feeReceiver && formControl.feeReceiver.isValid) { + setFormControl({ + ...formControl, + feeReceiver: { + ...formControl.feeReceiver, + needsUpdate: false, + } + }) + } + }, [formControl.feeReceiver.value, formControl.feeReceiver.isValid]) + + if (!selectedVault) return null + return ( + <> + + + Manage {selectedVault.name} + + + + handleFeeReceiverChange(e.target.value)} + handleClick={(address) => setFormControl({ feeReceiver: { ...formControl.feeReceiver, isValid: true, value: address } })} + placeholder='GAFS3TLVM...' + invalid={!formControl.feeReceiver.isValid} + description={dropdownData.feeReceiver.description} + /> + + + {formControl.feeReceiver.needsUpdate && + + } + + + + + ) +} diff --git a/apps/dapp/src/components/ManageVaults/InspectVault.tsx b/apps/dapp/src/components/ManageVaults/InspectVault.tsx index 2678d439..d61e2f48 100644 --- a/apps/dapp/src/components/ManageVaults/InspectVault.tsx +++ b/apps/dapp/src/components/ManageVaults/InspectVault.tsx @@ -11,6 +11,8 @@ import { Button, Grid, GridItem, HStack, Icon, Stack, Text } from "@chakra-ui/re import { DialogBody, DialogContent, DialogFooter, DialogHeader } from "../ui/dialog" import { FaRegEdit } from "react-icons/fa" import { IoClose } from "react-icons/io5" +import { ModalContext } from "@/contexts" +import { useContext } from "react" export const InspectVault = ({ @@ -24,6 +26,7 @@ export const InspectVault = ({ }) => { const selectedVault: VaultData | undefined = useAppSelector(state => state.wallet.vaults.selectedVault) const { address } = useSorobanReact() + const { editVaultModal: editModal } = useContext(ModalContext) if (!selectedVault) return null return ( @@ -34,11 +37,11 @@ export const InspectVault = ({

Inspect {selectedVault?.name ? selectedVault.name : shortenAddress(selectedVault.address)}

{address === selectedVault.manager && - - { handleOpenDeployVault('edit_vault', true, selectedVault) }} css={{ cursor: "pointer" }}> - - - + + { editModal.setIsOpen(true) }} css={{ cursor: "pointer" }}> + + + } diff --git a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx index 9fbe7223..aee2fc75 100644 --- a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx @@ -26,10 +26,17 @@ import { Input, Stack, } from "@chakra-ui/react" +import { EditVaultModal } from "../InteractWithVault/EditVault" export const ManageVaults = () => { const { address, activeChain } = useSorobanReact() - const { inspectVaultModal: inspectModal, deployVaultModal: deployModal, interactWithVaultModal: interactModal, transactionStatusModal: txModal } = useContext(ModalContext) + const { + inspectVaultModal: inspectModal, + deployVaultModal: deployModal, + interactWithVaultModal: interactModal, + transactionStatusModal: txModal, + editVaultModal: editModal + } = useContext(ModalContext) const dispatch = useAppDispatch() const modalContext = useContext(ModalContext) const vaults: VaultData[] = useAppSelector(state => state.wallet.vaults.createdVaults) @@ -155,6 +162,15 @@ export const ManageVaults = () => { onClose={() => { inspectModal.setIsOpen(false) }} /> + { editModal.setIsOpen(e.open) }} + size={'lg'} + placement={'center'} + > + + + { txModal.setIsOpen(e.open) }} diff --git a/apps/dapp/src/components/ui/button.tsx b/apps/dapp/src/components/ui/button.tsx new file mode 100644 index 00000000..21d5f4b5 --- /dev/null +++ b/apps/dapp/src/components/ui/button.tsx @@ -0,0 +1,40 @@ +import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" +import { + AbsoluteCenter, + Button as ChakraButton, + Span, + Spinner, +} from "@chakra-ui/react" +import * as React from "react" + +interface ButtonLoadingProps { + loading?: boolean + loadingText?: React.ReactNode +} + +export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} + +export const Button = React.forwardRef( + function Button(props, ref) { + const { loading, disabled, loadingText, children, ...rest } = props + return ( + + {loading && !loadingText ? ( + <> + + + + {children} + + ) : loading && loadingText ? ( + <> + + {loadingText} + + ) : ( + children + )} + + ) + }, +) diff --git a/apps/dapp/src/contexts/index.ts b/apps/dapp/src/contexts/index.ts index 8a96de99..b4d4e28d 100644 --- a/apps/dapp/src/contexts/index.ts +++ b/apps/dapp/src/contexts/index.ts @@ -40,6 +40,7 @@ export type ModalContextType = { deployVaultModal: ToggleModalProps, inspectVaultModal: ToggleModalProps, interactWithVaultModal: ToggleModalProps, + editVaultModal: ToggleModalProps, }; export const ModalContext = React.createContext({ @@ -72,5 +73,9 @@ export const ModalContext = React.createContext({ interactWithVaultModal: { isOpen: false, setIsOpen: () => {}, - } + }, + editVaultModal: { + isOpen: false, + setIsOpen: () => {}, + }, }); \ No newline at end of file diff --git a/apps/dapp/src/hooks/useVault.ts b/apps/dapp/src/hooks/useVault.ts index c5d6a1d6..d9062f72 100644 --- a/apps/dapp/src/hooks/useVault.ts +++ b/apps/dapp/src/hooks/useVault.ts @@ -23,6 +23,7 @@ export enum VaultMethod { GETASSETAMMOUNT = "get_asset_amounts_for_dftokens", GETIDLEFUNDS = "fetch_current_idle_funds", GETINVESTEDFUNDS = "fetch_current_invested_funds", + SETFEERECIEVER = "set_fee_receiver", } const isObject = (val: unknown) => typeof val === 'object' && val !== null && !Array.isArray(val); diff --git a/apps/dapp/src/providers/modal-provider.tsx b/apps/dapp/src/providers/modal-provider.tsx index 8564ab67..669ffa29 100644 --- a/apps/dapp/src/providers/modal-provider.tsx +++ b/apps/dapp/src/providers/modal-provider.tsx @@ -17,6 +17,7 @@ export const ModalProvider = ({ const [isDeployVaultModalOpen, setIsDeployVaultModalOpen] = React.useState(false) const [isInspectVaultModalOpen, setIsInspectVaultModalOpen] = React.useState(false) const [isInteractWithVaultModalOpen, setIsInteractWithVaultModalOpen] = React.useState(false) + const [isEditVaultModalOpen, setIsEditVaultModalOpen] = React.useState(false) const [isTransactionStatusModalOpen, setIsTransactionStatusModalOpen] = React.useState(false) const [transactionStatusModalStep, setTransactionStatusModalStep] = React.useState(0) @@ -101,6 +102,10 @@ export const ModalProvider = ({ isOpen: isInteractWithVaultModalOpen, setIsOpen: setIsInteractWithVaultModalOpen, }, + editVaultModal: { + isOpen: isEditVaultModalOpen, + setIsOpen: setIsEditVaultModalOpen, + }, } return ( diff --git a/apps/dapp/src/store/lib/features/walletStore.ts b/apps/dapp/src/store/lib/features/walletStore.ts index f03bd9be..7c9be113 100644 --- a/apps/dapp/src/store/lib/features/walletStore.ts +++ b/apps/dapp/src/store/lib/features/walletStore.ts @@ -104,7 +104,14 @@ export const walletSlice = createSlice({ vault.userBalance = action.payload.vaule } }) - } + }, + setVaultFeeReceiver: (state, action: PayloadAction) => { + state.vaults.createdVaults.forEach(vault => { + if (vault.address === state.vaults.selectedVault?.address) { + vault.feeReceiver = action.payload + } + }) + }, }, extraReducers(builder) { builder.addCase(fetchDefaultAddresses.pending, (state) => { @@ -131,7 +138,8 @@ export const { setVaults, setVaultTVL, resetSelectedVault, - setVaultUserBalance + setVaultUserBalance, + setVaultFeeReceiver } = walletSlice.actions // Other code such as selectors can use the imported `RootState` type