diff --git a/README.md b/README.md index d2498b95..aca64660 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ To deploy the factory contract run: ```sh bash run.sh cd apps/contracts -yarn deploy-factory +yarn deploy-factory ``` ### Publish addresses Once you have deployed an instance of the factory contract. You can publish the addresses to be used by anyone on the network. diff --git a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx index d6372293..1f8d5b44 100644 --- a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx +++ b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx @@ -52,6 +52,8 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const newVault: NewVaultState = useAppSelector(state => state.newVault); const strategies: Strategy[] = newVault.strategies; const indexName = useAppSelector(state => state.newVault.name) + const indexSymbol = useAppSelector(state => state.newVault.symbol) + const indexShare = useAppSelector(state => state.newVault.vaultShare) const managerString = useAppSelector(state => state.newVault.manager) const emergencyManagerString = useAppSelector(state => state.newVault.emergencyManager) const feeReceiverString = useAppSelector(state => state.newVault.feeReceiver) @@ -80,6 +82,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo && emergencyManagerString !== "" && feeReceiverString !== "" && assets.length > 0 + && !indexShare && loadingAssets === false ) { setDeployDisabled(false); @@ -105,6 +108,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo }) ); const assetsArray = await Promise.all(assetsPromises); + console.log(assetsArray) setAssets(assetsArray); setLoadingAssets(false); } catch (error) { @@ -119,6 +123,14 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const deployDefindex = async () => { + if (indexName === "" || indexName === undefined) { + console.log("Set index name please") + return + } + if (indexSymbol === "" || indexSymbol === undefined) { + console.log("Set index symbol please") + return + } if (managerString === "" || managerString === undefined) { console.log("Set manager please") return @@ -131,14 +143,26 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo console.log("Set fee receiver please") return } - + const vaultName = nativeToScVal(indexName, { type: "string" }) + const vaultSymbol = nativeToScVal(indexSymbol, { type: "string" }) + const vaultShare = nativeToScVal(indexShare, { type: "u32" }) const emergencyManager = new Address(emergencyManagerString) const feeReceiver = new Address(feeReceiverString) const manager = new Address(managerString) const salt = randomBytes(32) const ratios = [1]; - + /* + pub struct AssetAllocation { + pub address: Address, + pub strategies: Vec, + } + pub struct Strategy { + pub address: Address, + pub name: String, + pub paused: bool, + } + */ const strategyParamsScVal = strategies.map((param) => { return xdr.ScVal.scvMap([ new xdr.ScMapEntry({ @@ -149,18 +173,47 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo key: xdr.ScVal.scvSymbol('name'), val: nativeToScVal(param.name, { type: "string" }), }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('paused'), + val: nativeToScVal(false, { type: "bool" }), + }), ]); }); const strategyParamsScValVec = xdr.ScVal.scvVec(strategyParamsScVal); + const assetParamsScVal = assets.map((asset) => { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('address'), + val: new Address(asset).toScVal(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('strategies'), + val: strategyParamsScValVec, + }), + ]); + }); + + + /* fn create_defindex_vault( + emergency_manager: address, + fee_receiver: address, + vault_share: u32, + vault_name: string, + vault_symbol: string, + manager: address, + assets: vec, + salt: bytesn<32>) -> result + */ const createDefindexParams: xdr.ScVal[] = [ emergencyManager.toScVal(), feeReceiver.toScVal(), + vaultShare, + vaultName, + vaultSymbol, manager.toScVal(), - xdr.ScVal.scvVec(assets.map((token) => new Address(token).toScVal())), - xdr.ScVal.scvVec(ratios.map((ratio) => nativeToScVal(ratio, { type: "u32" }))), - strategyParamsScValVec, + xdr.ScVal.scvVec(assetParamsScVal), nativeToScVal(salt), ]; diff --git a/apps/dapp/src/components/DeployVault/DeployVault.tsx b/apps/dapp/src/components/DeployVault/DeployVault.tsx index b698a579..8d84f409 100644 --- a/apps/dapp/src/components/DeployVault/DeployVault.tsx +++ b/apps/dapp/src/components/DeployVault/DeployVault.tsx @@ -13,7 +13,7 @@ import ItemSlider from './Slider' import AddNewStrategyButton from './AddNewStrategyButton' import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks' import { ConfirmDelpoyModal } from './ConfirmDelpoyModal' -import { setName } from '@/store/lib/features/vaultStore' +import { setName, setSymbol } from '@/store/lib/features/vaultStore' import { Strategy } from '@/store/lib/features/walletStore' import { DialogBody, DialogCloseTrigger, DialogContent, DialogFooter, DialogHeader, DialogRoot, DialogTitle } from '../ui/dialog' @@ -21,6 +21,8 @@ export const DeployVault = () => { const dispatch = useAppDispatch() const strategies: Strategy[] = useAppSelector(state => state.newVault.strategies) const totalValues = useAppSelector(state => state.newVault.totalValues) + const vaultName = useAppSelector(state => state.newVault.name) + const vaultSymbol = useAppSelector(state => state.newVault.symbol) const [openConfirm, setOpenConfirm] = useState(false) const handleClose = () => { @@ -31,6 +33,10 @@ export const DeployVault = () => { await dispatch(setName(e.target.value)) } + const setVaultSymbol = async (e: any) => { + await dispatch(setSymbol(e.target.value)) + } + return ( @@ -39,9 +45,13 @@ export const DeployVault = () => { alignSelf={'end'} alignContent={'center'} mb={4} + gap={6} > - - + + + + + @@ -60,17 +70,16 @@ export const DeployVault = () => { setOpenConfirm(e.open)}> - - - + diff --git a/apps/dapp/src/components/DeployVault/VaultPreview.tsx b/apps/dapp/src/components/DeployVault/VaultPreview.tsx index 8b8a0b25..d93a71e5 100644 --- a/apps/dapp/src/components/DeployVault/VaultPreview.tsx +++ b/apps/dapp/src/components/DeployVault/VaultPreview.tsx @@ -9,19 +9,31 @@ import { IconButton, Fieldset, Stack, + Icon, } from '@chakra-ui/react' import { shortenAddress } from '@/helpers/shortenAddress' import { ChartData } from './ConfirmDelpoyModal' -import { setEmergencyManager, setFeeReceiver, setManager } from '@/store/lib/features/vaultStore' -import { useAppDispatch } from '@/store/lib/storeHooks' +import { setEmergencyManager, setFeeReceiver, setManager, setVaultShare } from '@/store/lib/features/vaultStore' +import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks' import { StrKey } from '@stellar/stellar-sdk' import { FaRegPaste } from "react-icons/fa6"; import { useSorobanReact } from '@soroban-react/core' import { InputGroup } from '../ui/input-group' import { Tooltip } from '../ui/tooltip' +import { + AccordionItem, + AccordionItemContent, + AccordionItemTrigger, + AccordionRoot, +} from "@chakra-ui/react" +import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io' - +enum AccordionItems { + STRATEGY_DETAILS = 'strategy-details', + MANAGER_CONFIGS = 'manager-configs', + FEES_CONFIGS = 'fees-configs', +} interface FormControlInterface { manager: { isValid: boolean | undefined; @@ -35,11 +47,88 @@ interface FormControlInterface { isValid: boolean | undefined; value: string | undefined; }, + vaultShare: number +} +const CustomAccordionTrigger = ({ title, type, accordionValue, setAccordionValue }: { title: string, type: AccordionItems, accordionValue: AccordionItems[], setAccordionValue: React.Dispatch> }) => { + return ( + { + if (accordionValue[0] === type) { + setAccordionValue([]) + } + }}> + + + + {title} settings + + + + + {accordionValue[0] === type ? + + + + : + + + + } + + + + + ) +} + +const CustomInputField = ({ + label, + value, + onChange, + handleClick, + placeholder, + invalid +}: { + label: string, + value: string, + onChange: (e: any) => void, + handleClick: (address: string) => void, + placeholder: string, + invalid: boolean +}) => { + const { address } = useSorobanReact() + if (!address) return null + return ( + + {label} + + + handleClick(address)} + > + + + + }> + + + + A valid Stellar / Soroban address is required. + + ) } export const VaultPreview = ({ data }: { data: ChartData[] }) => { const dispatch = useAppDispatch() const { address } = useSorobanReact() + const vaultShare = useAppSelector(state => state.newVault.vaultShare) + const [accordionValue, setAccordionValue] = useState([AccordionItems.STRATEGY_DETAILS]) const [formControl, setFormControl] = useState({ manager: { isValid: undefined, @@ -53,6 +142,7 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { isValid: undefined, value: undefined }, + vaultShare: 0 }) const isValidAddress = (address: string) => { if (StrKey.isValidEd25519PublicKey(address) || StrKey.isValidMed25519PublicKey(address) || StrKey.isValidContract(address)) { @@ -134,6 +224,17 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { }) dispatch(setFeeReceiver(input)) }; + const handleVaultShareChange = (input: any) => { + if (isNaN(input)) return + if (input < 0 || input > 100) return + const decimalRegex = /^(\d+)?(\.\d{0,2})?$/ + if (!decimalRegex.test(input)) return + setFormControl({ + ...formControl, + vaultShare: input + }) + dispatch(setVaultShare(input * 100)) + } return ( <> @@ -148,126 +249,88 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { height={200} /> */} - - Strategies - - - - - Name - Address - Percentage - - - - {data.map((strategy: ChartData, index: number) => ( - - {strategy.label} - - - {strategy.address ? shortenAddress(strategy.address) : '-'} - - - {strategy.value}% - - ))} - - - - - - - Manager - - - handleManagerChange(address!)} - > - - - - }> - handleManagerChange(event?.target.value)} - value={formControl.manager.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - Emergency manager - - - handleEmergencyManagerChange(address!)} - > - - - - }> - handleEmergencyManagerChange(event?.target.value)} - value={formControl.emergencyManager.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - Fee reciever - - - handleFeeReceiverChange(address!)} - > - - - - }> - handleFeeReceiverChange(event?.target.value)} - value={formControl.feeReceiver.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - + setAccordionValue(e.value)}> + + + + + + + Name + Address + Percentage + + + + {data.map((strategy: ChartData, index: number) => ( + + {strategy.label} + + {strategy.address ? shortenAddress(strategy.address) : '-'} + + {strategy.value}% + + ))} + + + + + + + + handleManagerChange(e.target.value)} + handleClick={(address: string) => handleManagerChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.manager.isValid === false} + /> + handleEmergencyManagerChange(e.target.value)} + handleClick={(address: string) => handleEmergencyManagerChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.emergencyManager.isValid === false} + /> + + + + + + handleFeeReceiverChange(e.target.value)} + handleClick={(address: string) => handleFeeReceiverChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.feeReceiver.isValid === false} + /> + + { handleVaultShareChange(e.target.value) }} + /> + + + + ) -} +} \ No newline at end of file diff --git a/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx b/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx index 20f4fcd9..c54896a8 100644 --- a/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx +++ b/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx @@ -27,33 +27,24 @@ export const InteractWithVault = () => { const vaultOperation = async () => { if (!address || !vaultMethod) return; - if (vaultMethod != VaultMethod.EMERGENCY_WITHDRAW) return; + if (!amount) throw new Error('Amount is required'); + if (vaultMethod != VaultMethod.DEPOSIT) return; + const depositParams: xdr.ScVal[] = [ + xdr.ScVal.scvVec([nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" })]), + xdr.ScVal.scvVec([nativeToScVal(((amount * 0.9) * Math.pow(10, 7)), { type: "i128" })]), + new Address(address).toScVal(), + ] console.log('Vault method:', vaultMethod) - const args: xdr.ScVal[] = [ - new Address(selectedVault.address).toScVal() - ]; - if (vaultMethod === VaultMethod.EMERGENCY_WITHDRAW) { - if (!selectedVault?.totalValues) throw new Error('Total values is required'); - args.unshift(nativeToScVal((0), { type: "i128" }),) + try { const result = await vault( - VaultMethod.EMERGENCY_WITHDRAW, + vaultMethod!, selectedVault?.address!, - [ - new Address(selectedVault.address).toScVal() - ], + depositParams, true, ) - return result - } else { - if (!amount) throw new Error('Amount is required'); - args.unshift(nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" }),) + } catch (error) { + console.error('Error:', error) } - const result = await vault( - vaultMethod!, - selectedVault?.address!, - args, - true, - ) } const setAmount = (e: any) => { diff --git a/apps/dapp/src/components/ManageVaults/AllVaults.tsx b/apps/dapp/src/components/ManageVaults/AllVaults.tsx index 49b68b77..4e9c636f 100644 --- a/apps/dapp/src/components/ManageVaults/AllVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/AllVaults.tsx @@ -71,17 +71,20 @@ export const AllVaults = ({ vault(VaultMethod.GETEMERGENCYMANAGER, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETFEERECEIVER, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETNAME, selectedVault, undefined, false).then((res: any) => scValToNative(res)), - vault(VaultMethod.GETSTRATEGIES, selectedVault, undefined, false).then((res: any) => scValToNative(res)), + vault(VaultMethod.GETASSETS, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETTOTALVALUES, selectedVault, undefined, false).then((res: any) => scValToNative(res)), ]); + const TVValues = Object.values(totalValues) + const totalValuesArray = TVValues.map((value: any) => Number(value)) + const accTotalValues = totalValuesArray.reduce((a: number, b: number) => a + b, 0) const newData: VaultData = { name: name || '', address: selectedVault, manager: manager, emergencyManager: emergencyManager, feeReceiver: feeReceiver, - strategies: strategies || [], - totalValues: totalValues || 0, + strategies: strategies[0].strategies || [], + totalValues: accTotalValues || 0, } return newData } catch (e: any) { @@ -183,7 +186,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.DEPOSIT, vault)} @@ -194,10 +196,8 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.WITHDRAW, vault)} > @@ -244,7 +244,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.DEPOSIT, vault)} @@ -254,7 +253,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.WITHDRAW, vault)} @@ -265,7 +263,6 @@ export const AllVaults = ({ {(address == vault.manager) && handleOpenDeployVault('edit_vault', true, vault)} @@ -276,7 +273,6 @@ export const AllVaults = ({ {(address == vault.emergencyManager || address == vault.manager) && handleOpenDeposit(VaultMethod.EMERGENCY_WITHDRAW, vault)} diff --git a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx index 13d27620..d406b036 100644 --- a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx @@ -51,7 +51,6 @@ export const ManageVaults = () => { const selectedVault = vaults.find(vault => vault.address === args.address) if (!selectedVault) return; for (const item of selectedVault.strategies) { - console.log(item) const newStrategy: Strategy = { ...item, share: selectedVault.strategies.length > 1 ? 100 / selectedVault.strategies.length : 100 }; await dispatch(pushStrategy(newStrategy)) } diff --git a/apps/dapp/src/components/ui/number-input.tsx b/apps/dapp/src/components/ui/number-input.tsx new file mode 100644 index 00000000..b178664c --- /dev/null +++ b/apps/dapp/src/components/ui/number-input.tsx @@ -0,0 +1,23 @@ +import { NumberInput as ChakraNumberInput } from "@chakra-ui/react" +import { forwardRef } from "react" + +export interface NumberInputProps extends ChakraNumberInput.RootProps {} + +export const NumberInputRoot = forwardRef( + function NumberInput(props, ref) { + const { children, ...rest } = props + return ( + + {children} + + + + + + ) + }, +) + +export const NumberInputField = ChakraNumberInput.Input +export const NumberInputScruber = ChakraNumberInput.Scrubber +export const NumberInputLabel = ChakraNumberInput.Label diff --git a/apps/dapp/src/hooks/useVault.ts b/apps/dapp/src/hooks/useVault.ts index 37d76f95..4490a9d4 100644 --- a/apps/dapp/src/hooks/useVault.ts +++ b/apps/dapp/src/hooks/useVault.ts @@ -13,8 +13,8 @@ export enum VaultMethod { GETFEERECEIVER = "get_fee_receiver", EMERGENCY_WITHDRAW = "emergency_withdraw", GETNAME= "name", - GETTOTALVALUES = "current_invested_funds", - GETSTRATEGIES = "get_strategies", + GETTOTALVALUES = "fetch_current_invested_funds", + GETASSETS = "get_assets", } const isObject = (val: unknown) => typeof val === 'object' && val !== null && !Array.isArray(val); diff --git a/apps/dapp/src/store/lib/features/vaultStore.ts b/apps/dapp/src/store/lib/features/vaultStore.ts index 1aae364a..62e3c1e6 100644 --- a/apps/dapp/src/store/lib/features/vaultStore.ts +++ b/apps/dapp/src/store/lib/features/vaultStore.ts @@ -1,16 +1,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import type { RootState } from '../store' -import axios from 'axios' import { getRemoteConfig } from '@/helpers/getRemoteConfig'; import { Strategy } from './walletStore'; export interface NewVaultState { address: string; - emergencyManager?: string; - feeReceiver?: string; - manager?: string; + emergencyManager: string; + feeReceiver: string; + manager: string; + vaultShare: number; name: string; + symbol: string; strategies: Strategy[]; totalValues?: number; } @@ -22,6 +23,8 @@ const initialState: NewVaultState = { feeReceiver: "", manager: "", name: "", + symbol: "", + vaultShare: 0, strategies: [ { address: "", @@ -103,6 +106,9 @@ export const newVaultSlice = createSlice({ setName: ((state, action: PayloadAction) => { state.name = action.payload; }), + setSymbol: ((state, action: PayloadAction) => { + state.symbol = action.payload; + }), setManager: ((state, action: PayloadAction) => { state.manager = action.payload; }), @@ -112,6 +118,9 @@ export const newVaultSlice = createSlice({ setFeeReceiver: ((state, action: PayloadAction) => { state.feeReceiver = action.payload; }), + setVaultShare: ((state, action: PayloadAction) => { + state.vaultShare = action.payload; + }), } }) @@ -122,9 +131,11 @@ export const { setStrategyValue, resetStrategyValue, setName, + setSymbol, setManager, setEmergencyManager, - setFeeReceiver + setFeeReceiver, + setVaultShare } = newVaultSlice.actions // Other code such as selectors can use the imported `RootState` type diff --git a/apps/dapp/src/utils/factory.ts b/apps/dapp/src/utils/factory.ts index 007987d5..76619c28 100644 --- a/apps/dapp/src/utils/factory.ts +++ b/apps/dapp/src/utils/factory.ts @@ -12,6 +12,7 @@ export async function fetchFactoryAddress(network: string): Promise { if (network !== "testnet" && network !== "mainnet") { throw new Error(`Invalid network: ${network}. It should be testnet or mainnet`); } + const url = `https://raw.githubusercontent.com/paltalabs/defindex/refs/heads/main/public/${network}.contracts.json`; try { const response = await fetch(url);