diff --git a/src/app/store.js b/src/app/store.js index bf7b2eefa..5a6e8a888 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,6 +1,7 @@ import { configureStore } from "@reduxjs/toolkit"; import stakeReducer from "../features/staking/stakeSlice"; import proposalsReducer from "../features/gov/govSlice"; +import nodeReducer from "../features/node/nodeSlice"; import feegrantReducer from "../features/feegrant/feegrantSlice"; import authzReducer from "../features/authz/authzSlice"; import bankReducer from "../features/bank/bankSlice"; @@ -27,6 +28,7 @@ export const store = configureStore({ group: groupSlice, multisig: multiSlice, slashing: slashingSlice, + node: nodeReducer, auth: authReducer, }, middleware: (getDefaultMiddleware) => diff --git a/src/components/AppDrawer.jsx b/src/components/AppDrawer.jsx index 48cfbbaac..03e59f4c1 100644 --- a/src/components/AppDrawer.jsx +++ b/src/components/AppDrawer.jsx @@ -10,7 +10,7 @@ import LogoutOutlinedIcon from "@mui/icons-material/LogoutOutlined"; import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined"; import Button from "@mui/material/Button"; import PropTypes from "prop-types"; -import { drawerListItems } from "./drawerListItems"; +import { DrawerListItems } from "./DrawerListItems"; import { parseBalance } from "../utils/denom"; import { shortenAddress } from "../utils/util"; import { useLocation } from "react-router-dom"; @@ -191,13 +191,13 @@ export default function AppDrawer(props) { - {drawerListItems( - location.pathname, - (path) => { - onNavigate(path); - }, - selectedNetwork?.showAirdrop - )} + { + onNavigate(path) + }} + showAirdrop={selectedNetwork?.showAirdrop} + /> ); diff --git a/src/components/drawerListItems.js b/src/components/DrawerListItems.jsx similarity index 73% rename from src/components/drawerListItems.js rename to src/components/DrawerListItems.jsx index afde9fac7..0c2c9bf60 100644 --- a/src/components/drawerListItems.js +++ b/src/components/DrawerListItems.jsx @@ -8,8 +8,34 @@ import LayersIcon from "@mui/icons-material/Layers"; import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; import DocumentScannerOutlinedIcon from "@mui/icons-material/DocumentScannerOutlined"; import GroupsOutlinedIcon from "@mui/icons-material/GroupsOutlined"; +import { useDispatch, useSelector } from "react-redux"; +import { getNodeInfo } from "../features/node/nodeSlice"; + +export function DrawerListItems({ currentPath, onNavigate, showAirdrop }) { + const dispatch = useDispatch(); + const [nodeDataInfo, setNodeDataInfo] = React.useState(false); + + const wallet = useSelector(state => state?.wallet); + const { chainInfo } = wallet; + const nodeInfo = useSelector(state => state?.node) + + React.useEffect(() => { + dispatch(getNodeInfo({ baseURL: chainInfo?.config?.rest })) + }, []) + + React.useEffect(() => { + if (nodeInfo?.nodeInfo?.status === 'idle') { + if (nodeInfo?.nodeInfo?.data?.application_version) { + let version = nodeInfo?.nodeInfo?.data?.application_version?.version; + + if (version?.indexOf('46') >= 0) { + setNodeDataInfo(true); + } else setNodeDataInfo(false); + } + } + }, [nodeInfo?.status]) + -export function drawerListItems(currentPath, onNavigate, showAirdrop) { return ( <> onNavigate("/group")} sx={{ pb: 0.5, pt: 0.5 }} selected={currentPath === "/group"} @@ -82,7 +108,12 @@ export function drawerListItems(currentPath, onNavigate, showAirdrop) { - + {showAirdrop ? ( diff --git a/src/components/group/AlertMsg.tsx b/src/components/group/AlertMsg.tsx new file mode 100644 index 000000000..ad81c3459 --- /dev/null +++ b/src/components/group/AlertMsg.tsx @@ -0,0 +1,30 @@ +import { Alert, Card, Typography } from '@mui/material' +import React from 'react' +import { gpStyles } from './groupCmpStyles' + +interface AlertMsgProps { + text: string, + type: string +} + +const AlertMsg = ({ text, type = 'error' }: AlertMsgProps) => { + return ( + + { + type === 'info' && + {text} + + } + + { + type === 'error' && + {text} + + } + + ) +} + +export default AlertMsg \ No newline at end of file diff --git a/src/components/group/CardSkeleton.tsx b/src/components/group/CardSkeleton.tsx new file mode 100644 index 000000000..46d36993f --- /dev/null +++ b/src/components/group/CardSkeleton.tsx @@ -0,0 +1,42 @@ +import { Grid, Paper, Skeleton } from '@mui/material' +import Typography, { TypographyProps } from '@mui/material/Typography'; +import { experimentalStyled as styled } from '@mui/material/styles'; +import React from 'react' + +const Item = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + padding: theme.spacing(2), + textAlign: 'center', + color: theme.palette.text.secondary, +})); + +function CardSkeleton() { + const variant = [ + 'h5', + 'h6', + 'h1', + ] as readonly TypographyProps['variant'][]; + const variants = [{ variant }, { variant }, { variant }] + + return ( + + {variants.map((variant) => ( + + + { + variant?.variant.map(v => ( + + + + )) + } + + + ))} + + ) +} + +export default CardSkeleton \ No newline at end of file diff --git a/src/components/group/DialogVote.tsx b/src/components/group/DialogVote.tsx new file mode 100644 index 000000000..8623a0711 --- /dev/null +++ b/src/components/group/DialogVote.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Avatar from '@mui/material/Avatar'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Dialog from '@mui/material/Dialog'; +import { ListItemButton } from '@mui/material'; + +const options = [{ + label: 'Yes', + value: 1, + active: '#6d70fe' +}, { + label: 'No', + value: 3, + active: '#e87d91' +}, { + label: 'No with Veto', + value: 4, + active: 'red' +}, { + label: 'Abstain', + value: 2, + active: '#6e81cb' +}] + +interface voteprops { + vote: number, proposalId: string +} +export interface SimpleDialogProps { + open: boolean; + proposalId: string; + selectedValue: string; + voteRes: any; + onConfirm: (obj: voteprops) => void; + onClose: (value: string) => void; +} + +export default function DailogVote(props: SimpleDialogProps) { + const { onClose, voteRes, proposalId, onConfirm, selectedValue, open } = props; + const [vote, setVote] = React.useState(0); + + const handleClose = () => { + onClose(selectedValue); + }; + + const handleListItemClick = (value: number) => { + setVote(value); + }; + + return ( + + Vote for Proposal # {proposalId} + + {options.map((option) => ( + handleListItemClick(option?.value)} key={option?.label}> + + + ))} + + + + + + + + ); +} + diff --git a/src/components/group/FileProposalOptions.tsx b/src/components/group/FileProposalOptions.tsx new file mode 100644 index 000000000..fcd5baef7 --- /dev/null +++ b/src/components/group/FileProposalOptions.tsx @@ -0,0 +1,125 @@ +import { Button, Grid } from '@mui/material'; +import { Box } from '@mui/system'; +import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; +import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; +import React from 'react'; + + +const MULTISIG_SEND_TEMPLATE = "https://resolute.witval.com/_static/send.csv"; +const MULTISIG_DELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/delegate.csv"; +const MULTISIG_UNDELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/undelegate.csv"; +const MULTISIG_REDELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/redelegate.csv"; + + +const TYPE_SEND = "SEND"; +const TYPE_DELEGATE = "DELEGATE"; +const TYPE_UNDELEGATE = "UNDELEGATE"; +const TYPE_REDELEGATE = "REDELEGATE"; + +interface FileProposalOptionsProps { + txType: string, + onFileContents: any +} + +function FileProposalOptions({ + txType, + onFileContents +}: FileProposalOptionsProps) { + + return ( + + + + + + ) +} + +export default FileProposalOptions \ No newline at end of file diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx new file mode 100644 index 000000000..f48e1aa74 --- /dev/null +++ b/src/components/group/GroupCard.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import { Box } from '@mui/system'; +import { shortenAddress } from '../../utils/util'; +import { getLocalTime } from '../../utils/datetime'; +import ReadMoreIcon from '@mui/icons-material/ReadMore'; +import { IconButton, Paper } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; + +interface GroupCardProps { + group: any +} + +interface BoxTextProps { + label: any, + text: any +} + +const BoxText = ({ label, text }: BoxTextProps) => { + return ( + + + {label} + + + {text} + + + ) +} + +export default function GroupCard({ group }: GroupCardProps) { + const navigate = useNavigate(); + const [showFullText, setShowFullText] = React.useState(false); + + return ( + + + navigate(`/groups/${group?.id}`)} + color='primary' + sx={{ float: 'right' }} + > + + + + # {group?.id} + + + ##   + {!showFullText && group?.metadata?.substring(0, 30)} + + { + showFullText && group?.metadata + } + + {group?.metadata?.length > 40 ? setShowFullText(!showFullText)} + href='javascript:void(0);' + > {showFullText? ' ...show less': ' ...more'} : null} + + + + + + + + + ); +} diff --git a/src/components/group/GroupList.tsx b/src/components/group/GroupList.tsx new file mode 100644 index 000000000..c9b46140b --- /dev/null +++ b/src/components/group/GroupList.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Box, CircularProgress, Grid } from "@mui/material"; +import AlertMsg from "./AlertMsg"; +import GroupCard from "./GroupCard"; +import PaginationElement from "./PaginationElement"; + +export interface GroupsByAdminProps { + groups: any; + status: string; + total: number; + handlePagination: (key: number) => void; + onAction: (group: any) => void; + paginationKey: string; +} + +export default function GroupList(props: GroupsByAdminProps) { + const { groups, onAction, + paginationKey, + handlePagination, total, status } = props; + + return ( + <> + + + { + status === 'pending'? + : null + } + + { + status !== 'pending' && !groups.length ? + + : null + } + + {status !== 'pending' && groups?.map((group: any, index: any,) => ( + + + + ))} + + { + total > 9 && + + + } + + + + + ) +} diff --git a/src/components/group/GroupMemberCount.jsx b/src/components/group/GroupMemberCount.jsx new file mode 100644 index 000000000..9db6f5203 --- /dev/null +++ b/src/components/group/GroupMemberCount.jsx @@ -0,0 +1,37 @@ +import { useDispatch, useSelector } from "react-redux"; +import * as React from "react"; +import { getGroupMembers } from "../../features/group/groupSlice"; +import { CircularProgress } from "@mui/material"; + +function GroupMemberCount(props) { + const dispatch = useDispatch(); + const [memberCount, setMemberCount] = React.useState(0); + + const wallet = useSelector((state) => state.wallet); + const members = useSelector(state => state.group.members); + + const { groupId } = props; + console.log('memberssssssss', groupId, members, wallet?.chainInfo) + + React.useEffect(() => { + // const { chainInfo } = wallet; + + // const data = { + // baseURL: chainInfo?.config?.rest, + // groupId + // } + + // dispatch(getGroupMembers(data)) + }, []) + + return ( +
+ { + members?.status === 'loading' ? : + members?.pagination?.total || 0 + } +
+ ) +} + +export default GroupMemberCount \ No newline at end of file diff --git a/src/components/group/GroupTab.tsx b/src/components/group/GroupTab.tsx new file mode 100644 index 000000000..5d65b02f5 --- /dev/null +++ b/src/components/group/GroupTab.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import { Button, Paper } from '@mui/material'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; + +} + +export function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +interface GroupTabinterface { + handleTabChange: (newValue: number) => number; + tabs: Array<[]>; +} + +export default function GroupTab({ handleTabChange, tabs }: GroupTabinterface) { + const [value, setValue] = React.useState(0); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + handleTabChange(newValue) + }; + + return ( + + + + + { + tabs.map((t, i) => ( + + )) + } + {/* + */} + + + + ); +} diff --git a/src/components/group/MembersTable.js b/src/components/group/MembersTable.js new file mode 100644 index 000000000..27872ffe5 --- /dev/null +++ b/src/components/group/MembersTable.js @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import TablePagination from '@mui/material/TablePagination'; +import { IconButton, Tooltip, Typography } from '@mui/material'; +import DeleteOutline from '@mui/icons-material/DeleteOutline'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + textAlign: 'center', + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 16, + textAlign: 'center' + }, +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0, + }, +})); + +export default function MembersTable({ rows, total, + pageNumber = 0, limit, handleMembersPagination, handleDeleteMember }) { + const [page, setPage] = React.useState(pageNumber); + const [rowsPerPage, setRowsPerPage] = React.useState(limit); + const handleChangePage = (event, newPage) => { + setPage(newPage); + + handleMembersPagination(Number(newPage), rowsPerPage, rows?.pagination?.next_key) + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(+event.target.value); + setPage(+event.target.value); + + handleMembersPagination(Number(page), +event.target.value, '') + }; + + return ( + + Group Members
+ + + + Address + Weight + Metadata + + + + + {rows?.members?.map((row) => ( + + + {row?.member?.address || '-'} + + + {row?.member?.weight || '-'} + + + {row?.member?.metadata || '-'} + + + + { + handleDeleteMember({ + address: row?.member?.address, + weight: '0', + metadata: row?.member?.metadata + }) + }} color='error'> + + + + + + + ))} + +
+ +
+ ); +} diff --git a/src/components/group/PaginationElement.tsx b/src/components/group/PaginationElement.tsx new file mode 100644 index 000000000..7cdced770 --- /dev/null +++ b/src/components/group/PaginationElement.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import Pagination from '@mui/material/Pagination'; +import Stack from '@mui/material/Stack'; + +interface PaginationElementProps { + total: number; + handlePagination: (key: number) => void; + paginationKey: string; +} + +export default function PaginationElement({ total, + paginationKey, + handlePagination }: + PaginationElementProps) { + const [page, setPage] = React.useState(1); + const handleChange = (event: React.ChangeEvent, value: number) => { + + setPage(value); + handlePagination(value-1) + }; + + return ( + + + + ); +} diff --git a/src/components/group/PolicyCard.jsx b/src/components/group/PolicyCard.jsx new file mode 100644 index 000000000..f9b471565 --- /dev/null +++ b/src/components/group/PolicyCard.jsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import CardMedia from '@mui/material/CardMedia'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import Collapse from '@mui/material/Collapse'; +import Avatar from '@mui/material/Avatar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import { red } from '@mui/material/colors'; +import FavoriteIcon from '@mui/icons-material/Favorite'; +import ShareIcon from '@mui/icons-material/Share'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import RowItem from './RowItem'; +import { Grid, Paper } from '@mui/material'; +import { setLocalStorage, shortenAddress } from '../../utils/util'; +import ReadMoreIcon from '@mui/icons-material/ReadMore'; +import { useNavigate } from 'react-router-dom'; + +const ExpandMore = styled((props) => { + const { expand, ...other } = props; + return ; +})(({ theme, expand }) => ({ + transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), +})); + +const policyType = { + '/cosmos.group.v1.ThresholdDecisionPolicy': 'Threshold Decision Policy', + '/cosmos.group.v1.PercentageDecisionPolicy': 'Percentage Decision Policy', +} + +export default function PolicyCard({ obj }) { + const [expanded, setExpanded] = React.useState(false); + + const handleExpandClick = () => { + setExpanded(!expanded); + }; + + const navigate = useNavigate(); + + return ( + { + setLocalStorage('policy', obj, 'object'); + navigate(`/groups/${obj?.group_id}/policies/${obj?.address}`) + }} + sx={{m: 1, borderRadius: 2 }} + > + + + + } + title={policyType[obj?.decision_policy['@type']]} + subheader={shortenAddress(obj?.address, 19)} + /> + + + + ## {obj?.metadata || '-'} + + { + obj?.decision_policy['@type'] === '/cosmos.group.v1.ThresholdDecisionPolicy' ? + : + + } + + + + + + + ); +} diff --git a/src/components/group/PolicyDetails.tsx b/src/components/group/PolicyDetails.tsx new file mode 100644 index 000000000..cf1471d38 --- /dev/null +++ b/src/components/group/PolicyDetails.tsx @@ -0,0 +1,248 @@ +import { Grid, IconButton, TextField, Typography } from '@mui/material' +import { Box } from '@mui/system' +import React, { useEffect, useState } from 'react' +import EditIcon from '@mui/icons-material/Edit'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { getLocalTime } from '../../utils/datetime'; +import CheckIcon from '@mui/icons-material/Check'; +import { useSelector } from 'react-redux'; +import { ThresholdDecisionPolicy } from '../../utils/util'; + +interface GridItemProps { + label: string; + text: string; + isEditMode?: boolean; + handleUpdate: any +} + +const GridItemEdit = ({ + label, + text, + isEditMode, + handleUpdate +}: GridItemProps) => { + const [isEdit, setIsEdit] = useState(isEditMode); + + return ( + + + + {label} + + + + { + isEdit ? + setIsEdit(false)} + /> : + <> + + {text} +     + setIsEdit(true)} color='primary' /> + + } + + + + ) +} + +interface GridItemTextProps { + label: string; + text: string; + isEditMode?: boolean; + isEqualColumn?: boolean +} + +const GridItemText = ({ + label, + text, + isEqualColumn +}: GridItemTextProps) => { + return ( + + + + {label} + + + + + {text} + + + + ) +} + +interface EditTextFieldProps { + name?: string; + value: string; + hideShowEdit?: any; + placeholder?: string + handleUpdate?: any; +} + +const EditTextField = ({ + placeholder, + handleUpdate, + name, value, hideShowEdit, +}: EditTextFieldProps) => { + const [field, setField] = useState(value); + + return ( + + setField(e.target.value)} + value={field} fullWidth /> + + handleUpdate(field)} + color='primary' + sx={{ border: '1px solid', borderRadius: 2, ml: 2 }} + > + + + hideShowEdit()} + sx={{ border: '1px solid', borderRadius: 2, ml: 2 }}> + + + + ) +} + +interface PolicyDetailsProps { + policyObj: any, + handleUpdateAdmin: any, + handleUpdateMetadata: any +} + +function PolicyDetails({ + policyObj, + handleUpdateMetadata, + handleUpdateAdmin }: PolicyDetailsProps) { + const [isMetaEditMode, setIsMetaEditMode] = useState(false); + const [isAdminEdit, setIsAdminEdit] = useState(false); + + const updateMetadataRes = useSelector((state: any) => state?.group?.updateGroupMetadataRes); + + useEffect(() => { + if (updateMetadataRes?.status === 'idle') { + setIsMetaEditMode(false); + } + }, [updateMetadataRes?.status]) + + const updatePolicyAdminRes = useSelector((state: any) => state?.group?.updatePolicyAdminRes); + + useEffect(() => { + if (updatePolicyAdminRes?.status === 'idle') { + setIsAdminEdit(false); + } + }, [updatePolicyAdminRes?.status]) + + return ( + + + { + isMetaEditMode ? + { + setIsMetaEditMode(false); + }} + value={policyObj?.metadata} /> + : + + ## {policyObj?.metadata}      + setIsMetaEditMode(true)} /> + + } + + + + Note: Only admin can be update metadata. + + + + + + + + + Note: Only admin can be update admin address. + + + + + { + policyObj?.decision_policy['@type'] === ThresholdDecisionPolicy ? + : + + } + + + + + + + + + + + + + + + ) +} + +export default PolicyDetails \ No newline at end of file diff --git a/src/components/group/PolicyForm.tsx b/src/components/group/PolicyForm.tsx new file mode 100644 index 000000000..8147c173a --- /dev/null +++ b/src/components/group/PolicyForm.tsx @@ -0,0 +1,235 @@ +import { + Box, Button, TextField, Select, MenuItem, FormControlLabel, Switch, Typography, Grid, FormControl, InputLabel, InputAdornment, +} from '@mui/material'; +import React, { useState } from 'react' +import { useForm, Controller } from 'react-hook-form'; +import { Interface } from 'readline'; +import Policy from '../../pages/group/Policy'; +import { gpStyles } from './groupCmpStyles'; + +interface PolicyFormProps { + handlePolicy: any, + handlePolicyClose: any + policyObj?: any +} + +function PolicyForm({ handlePolicy, policyObj, handlePolicyClose }: PolicyFormProps) { + var policyInitialObj = { + metadata: '', + decisionPolicy: '', + threshold: 0, + percentage: 0, + votingPeriod: '', + minExecPeriod: 0 + }; + + if (policyObj) { + policyInitialObj = { + metadata: 'sample ' || policyObj?.metadata, + decisionPolicy: policyObj?.decision_policy?.['@type'] === '/cosmos.group.v1.ThresholdDecisionPolicy' ? + 'threshold' : 'percentage', + threshold: Number(policyObj?.decision_policy?.threshold || 0), + percentage: Number(policyObj?.decision_policy?.percentage || 0), + votingPeriod: '12' || parseFloat(policyObj?.decision_policy?.windows?.voting_period || 0), + minExecPeriod: 12 || parseFloat(policyObj?.decision_policy?.windows?.min_execution_period || 0) + } + } + + + console.log('intital---', policyInitialObj) + + const { register, control, + handleSubmit, + watch, + formState: { errors }, + reset, trigger, setError } = useForm({ + defaultValues: { + ...policyInitialObj + } + }); + + return ( + + + Add Decision Policy + +
+
+ + ( + + )} /> + + + + ( + + Decision Policy* + + + )} /> + + + + { + watch('decisionPolicy') === 'percentage' ? + ( + + + + )} /> + + : + ( + + + + )} /> + + } + + + ( + + Sec, + }} + /> + + )} /> + + + + ( + + Sec, + }} + /> + + )} /> + + + + + } + label="Group policy as admin" + labelPlacement="start" + /> + + if set to true, the group policy account address will be used as + group and policy admin + + + +
+ + + + + + +
+
+ ) +} + +export default PolicyForm \ No newline at end of file diff --git a/src/components/group/ProposalCard.tsx b/src/components/group/ProposalCard.tsx new file mode 100644 index 000000000..85777caab --- /dev/null +++ b/src/components/group/ProposalCard.tsx @@ -0,0 +1,131 @@ +import ReadMore from '@mui/icons-material/ReadMore' +import { Avatar, Chip, Grid, Paper, Stack, Typography } from '@mui/material' +import { deepOrange, deepPurple } from '@mui/material/colors' +import { Box } from '@mui/system' +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { getLocalTime } from '../../utils/datetime' +import { proposalStatus, shortenAddress } from '../../utils/util' + +function stringAvatar(name: string) { + return { + sx: { + color: '#000000', + bgcolor: deepOrange[50], + + }, + children: `${name}`, + }; +} + + +interface ProposalCardProps { + proposal: any +} + +function ProposalCard({ proposal }: ProposalCardProps) { + const proposalStatuses: any = proposalStatus; + const yes = parseFloat(proposal?.final_tally_result?.yes_count); + const no = parseFloat(proposal?.final_tally_result?.no_count); + const abstain = parseFloat(proposal?.final_tally_result?.abstain_count); + const veto = parseFloat(proposal?.final_tally_result?.no_with_veto_count); + + const sum = yes + no + abstain + veto; + const yesP = (yes / sum).toFixed(2); + const noP = (no / sum).toFixed(2); + const abP = (abstain / sum).toFixed(2) + const vetoP = (veto / sum).toFixed(2); + + + const navigate = useNavigate(); + + return ( + { + navigate(`/groups/proposals/${proposal?.id}`) + }} + sx={{ padding: 3 }} variant='outlined' elevation={1}> + + + + + + # {proposal?.metadata || '-'} + + + + + +
+ + + Voting End Time + + + {getLocalTime(proposal?.voting_period_end)} + + + + Submit Time + + + {getLocalTime(proposal?.submit_time)} + + + + + + Proposers + + { + proposal?.proposers?.map((p: string) => ( + + {p && shortenAddress(p, 21)} + + )) + } + + + Policy Address + + {proposal?.group_policy_address && shortenAddress(proposal?.group_policy_address, 21)} + + + + + + + + + +
+ ) +} + +export default ProposalCard \ No newline at end of file diff --git a/src/components/group/RowItem.jsx b/src/components/group/RowItem.jsx new file mode 100644 index 000000000..92188b8df --- /dev/null +++ b/src/components/group/RowItem.jsx @@ -0,0 +1,22 @@ +import { Grid, Typography } from '@mui/material' +import React from 'react' + +function RowItem({ lable, value, equal }) { + return ( + + + {lable} + + + {value} + + + ) +} + +export default RowItem \ No newline at end of file diff --git a/src/components/group/TxBasicFields.tsx b/src/components/group/TxBasicFields.tsx new file mode 100644 index 000000000..38d5c1069 --- /dev/null +++ b/src/components/group/TxBasicFields.tsx @@ -0,0 +1,92 @@ +import { Grid, TextField } from '@mui/material'; +import React from 'react' +import { Controller, useFormContext } from 'react-hook-form'; +import FeeComponent from '../multisig/FeeComponent'; + +interface TxBasicFieldsProps { + chainInfo: any +} + +function TxBasicFields({ + chainInfo +}: TxBasicFieldsProps) { + const { handleSubmit, control, + setValue, formState:{errors}, } = useFormContext(); + + return ( + + + ( + + )} + /> + + + ( + + )} + /> + + + { + setValue( + "fees", + Number(v) * + 10 ** + chainInfo?.config?.currencies[0].coinDecimals + ); + }} + chainInfo={chainInfo} + /> + + + ) +} + +export default TxBasicFields \ No newline at end of file diff --git a/src/components/group/TxTypeComponent.tsx b/src/components/group/TxTypeComponent.tsx new file mode 100644 index 000000000..c28e7cd77 --- /dev/null +++ b/src/components/group/TxTypeComponent.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import { Box } from '@mui/system'; +import { FormHelperText } from '@mui/material'; + +interface TxTypeComponentProps { + handleType: any +} + +export default function TxTypeComponent({ handleType }: TxTypeComponentProps) { + const [value, setValue] = React.useState(''); + + const handleRadioChange = (event: React.ChangeEvent) => { + setValue((event.target as HTMLInputElement).value); + handleType((event.target as HTMLInputElement).value); + }; + + return ( + + + + How do you want a add transaction? + + + } label="Manually" /> + } label="Upload through file" /> + + + Only one transaction at a time. + Multiple transactions at a time. + + + + + ); +} diff --git a/src/components/group/UpdateGroupMemberForm.tsx b/src/components/group/UpdateGroupMemberForm.tsx new file mode 100644 index 000000000..dd5b6bf15 --- /dev/null +++ b/src/components/group/UpdateGroupMemberForm.tsx @@ -0,0 +1,131 @@ + +import { + Box, TextField, IconButton, Grid, Button, +} from '@mui/material'; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import React from 'react'; +import AddIcon from '@mui/icons-material/Add'; + +interface UpdateGroupMemberFormProps { + members: any, + handleUpdate: any, + handleCancel: any +} + +export function UpdateGroupMemberForm({ + members, + handleUpdate, + handleCancel +}: UpdateGroupMemberFormProps) { + const { register, control, + handleSubmit, + watch, + formState: { errors }, + reset, trigger, setError } = useForm({ + defaultValues: { + members: members + } + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "members" + }); + + return ( + + +
+ { + fields.map((item, index) => { + return + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + remove(index)} color='error'> + + + { + append({ address: '', metadata: '', weight: 0 }) + }} color='primary'> + + + + + }) + } + + + + + +
+
+
+ ) +} + +export default UpdateGroupMemberForm diff --git a/src/components/group/VotesTable.js b/src/components/group/VotesTable.js new file mode 100644 index 000000000..dd49a7f92 --- /dev/null +++ b/src/components/group/VotesTable.js @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import TablePagination from '@mui/material/TablePagination'; +import { Typography } from '@mui/material'; +import { getLocalTime } from '../../utils/datetime'; + +const votesStatus = { + 'VOTE_OPTION_YES': { + label: 'Yes', + bgColor: '#d8ead8', + color: 'green' + }, + 'VOTE_OPTION_NO': { + label: 'NO', + bgColor: '#edcdcd', + color: 'red' + }, + 'VOTE_OPTION_ABSTAIN': { + label: 'ABSTAIN', + bgColor: '#cdd1ed', + color: '#0026ff' + }, + 'VOTE_OPTION_NO_WITH_VETO': { + label: 'NO WITH VETO', + bgColor: '#f2dbf1', + color: '#df00fa' + } +} + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + textAlign: 'center', + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 16, + textAlign: 'center' + }, +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0, + }, +})); + +export default function VotesTable({ rows, total, + pageNumber = 0, limit, handleMembersPagination }) { + const [page, setPage] = React.useState(pageNumber); + const [rowsPerPage, setRowsPerPage] = React.useState(limit); + const handleChangePage = (event, newPage) => { + setPage(newPage); + + handleMembersPagination(Number(newPage), rowsPerPage, rows?.pagination?.next_key) + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(+event.target.value); + setPage(+event.target.value); + + handleMembersPagination(Number(page), +event.target.value, '') + }; + + return ( + + Votes
+ + + + Voter + Option + Metdata + Submit Time + + + + {rows?.votes?.map((row) => ( + + + {row?.voter || '-'} + + + + {votesStatus[row?.option]?.label || '-'} + + + + {row?.metadata || '-'} + { + getLocalTime(row?.submit_time) || '-'} + + ))} + +
+ +
+ ); +} diff --git a/src/components/group/bulk/Delegate.jsx b/src/components/group/bulk/Delegate.jsx new file mode 100644 index 000000000..fb25f515d --- /dev/null +++ b/src/components/group/bulk/Delegate.jsx @@ -0,0 +1,126 @@ +import { Box, Button, InputAdornment, TextField } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { Decimal } from "@cosmjs/math"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; +import Autocomplete from "@mui/material/Autocomplete"; + +import { useForm, Controller, useFormContext } from "react-hook-form"; + +Delegate.propTypes = { + currency: PropTypes.object.isRequired, +}; + +export default function Delegate(props) { + const { currency } = props; + + const { handleSubmit, watch, control, + setValue, + formState: { errors }, } = useFormContext({ + defaultValues: { + amount: 0, + validator: null, + }, + }); + + var validators = useSelector((state) => state.staking.validators); + var [data, setData] = useState([]); + + useEffect(() => { + const data = []; + for (let i = 0; i < validators.activeSorted.length; i++) { + const validator = validators.active[validators.activeSorted[i]]; + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + }; + data.push(temp); + } + + for (let i = 0; i < validators.inactiveSorted.length; i++) { + const validator = validators.inactive[validators.inactiveSorted[i]]; + if (!validator.jailed) { + const temp = { + label: validator.description.moniker, + value: validators.inactiveSorted[i], + }; + data.push(temp); + } + } + + setData(data); + }, [validators]); + + return ( + <> + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + { + return Number(value) > 0; + }, + }} + render={({ field, fieldState: { error } }) => ( + + {currency?.coinDenom} + + ), + }} + /> + )} + /> + + ); +} diff --git a/src/components/group/bulk/RedelegateForm.jsx b/src/components/group/bulk/RedelegateForm.jsx new file mode 100644 index 000000000..61d4be4cf --- /dev/null +++ b/src/components/group/bulk/RedelegateForm.jsx @@ -0,0 +1,252 @@ +import { Box, Button, InputAdornment, TextField } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { Decimal } from "@cosmjs/math"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; +import Autocomplete from "@mui/material/Autocomplete"; +import { Redelegate } from "../../../txns/staking"; + +import { useForm, Controller, useFormContext } from "react-hook-form"; + +RedelegateForm.propTypes = { + chainInfo: PropTypes.object.isRequired, + address: PropTypes.string.isRequired, + onRedelegate: PropTypes.object.isRequired, +}; + +function parseDelegation(delegation, currency) { + return ( + parseFloat(delegation?.delegation?.shares) / + (10 ** currency?.coinDecimals).toFixed(6) + ); +} + +export default function RedelegateForm(props) { + const wallet = useSelector(state => state?.wallet); + + const { chainInfo } = wallet; + + const { handleSubmit, watch, control, + setValue, + formState: { errors }, } = useFormContext({ + defaultValues: { + amount: 0, + source: null, + destination: null, + }, + }); + + var validators = useSelector((state) => state.staking.validators); + const delegations = useSelector( + (state) => state.staking.delegations.delegations + ); + + var [data, setData] = useState([]); + + useEffect(() => { + const data = []; + + for (let i = 0; i < delegations.length; i++) { + if (validators.active[delegations[i].delegation.validator_address]) { + const temp = { + label: + validators.active[delegations[i].delegation.validator_address] + .description.moniker, + value: { + shares: parseDelegation( + delegations[i], + chainInfo.config.currencies[0] + ), + validator: delegations[i].delegation.validator_address, + }, + }; + data.push(temp); + } else if ( + validators.inactive[delegations[i].delegation.validator_address] + ) { + const temp = { + label: + validators.inactive[delegations[i].delegation.validator_address] + .description.moniker, + value: { + shares: parseDelegation( + delegations[i], + chainInfo.config.currencies[0] + ), + validator: delegations[i].delegation.validator_address, + }, + }; + data.push(temp); + } + } + + setData(data); + }, [delegations]); + + const currency = chainInfo.config.currencies[0]; + const onSubmit = (data) => { + const baseAmount = Decimal.fromUserInput( + data.amount, + Number(currency?.coinDecimals) + ).atomics; + + const msgRedelegate = Redelegate( + data.delegator, + data.source?.value?.validator, + data.destination?.value, + baseAmount, + chainInfo.config.currencies[0].coinMinimalDenom + ); + + props.onRedelegate(msgRedelegate); + }; + + const [vals, setVals] = useState([]); + useEffect(() => { + const data = []; + for (let i = 0; i < validators.activeSorted.length; i++) { + const validator = validators.active[validators.activeSorted[i]]; + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + }; + data.push(temp); + } + + for (let i = 0; i < validators.inactiveSorted.length; i++) { + const validator = validators.inactive[validators.inactiveSorted[i]]; + if (!validator.jailed) { + const temp = { + label: validator.description.moniker, + value: validators.inactiveSorted[i], + }; + data.push(temp); + } + } + + setVals(data); + }, [validators]); + + return ( + <> + ( + + )} + /> + ( + + option.value?.validator === value.value?.validator + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + + option.value === value.value + } + options={vals} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + { + return Number(value) > 0; + }, + }} + render={({ field, fieldState: { error } }) => ( + + {currency?.coinDenom} + + ), + }} + /> + )} + /> + + ); +} diff --git a/src/components/group/bulk/Send.jsx b/src/components/group/bulk/Send.jsx new file mode 100644 index 000000000..ff43e1be8 --- /dev/null +++ b/src/components/group/bulk/Send.jsx @@ -0,0 +1,96 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Button, InputAdornment, TextField } from "@mui/material"; +import { Decimal } from "@cosmjs/math"; +import { Box } from "@mui/system"; +import { useForm, Controller, useFormContext } from "react-hook-form"; +import { fromBech32 } from "@cosmjs/encoding"; + +Send.propTypes = { + chainInfo: PropTypes.object.isRequired, + address: PropTypes.string.isRequired, + onSend: PropTypes.object.isRequired, +}; + +export default function Send(props) { + const { currency } = props; + + const { handleSubmit, watch, control, + setValue, + formState: { errors }, } = useFormContext({ + defaultValues: { + amount: 0, + recipient: "", + }, + }); + + return ( + <> + { + try { + fromBech32(value); + return true; + } catch (error) { + return false; + } + }, + }} + render={({ field, fieldState: { error } }) => ( + + )} + /> + { + return Number(value) > 0; + }, + }} + render={({ field, fieldState: { error } }) => ( + + {currency?.coinDenom} + + ), + }} + /> + )} + /> + + ); +} diff --git a/src/components/group/bulk/UnDelegateForm.jsx b/src/components/group/bulk/UnDelegateForm.jsx new file mode 100644 index 000000000..7ddf7d091 --- /dev/null +++ b/src/components/group/bulk/UnDelegateForm.jsx @@ -0,0 +1,176 @@ +import { Box, Button, InputAdornment, TextField } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { Decimal } from "@cosmjs/math"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; +import { useForm, Controller, useFormContext } from "react-hook-form"; +import Autocomplete from "@mui/material/Autocomplete"; +import { UnDelegate } from "../../../txns/staking"; + +UnDelegateForm.propTypes = { + chainInfo: PropTypes.object.isRequired, + address: PropTypes.string.isRequired, + onUndelegate: PropTypes.func.isRequired, +}; + +function parseDelegation(delegation, currency) { + return ( + parseFloat(delegation?.delegation?.shares) / + (10 ** currency?.coinDecimals).toFixed(6) + ); +} + +export default function UnDelegateForm(props) { + const { address } = props; + + const wallet = useSelector(state => state?.wallet); + const { chainInfo } = wallet; + + const { + handleSubmit, + control, + formState: { errors }, + } = useFormContext({ + defaultValues: { + amount: 0, + validator: null, + delegator: address, + }, + }); + + const validators = useSelector((state) => state.staking.validators); + const delegations = useSelector( + (state) => state.staking.delegations.delegations + ); + var [data, setData] = useState([]); + + useEffect(() => { + const data = []; + + for (let i = 0; i < delegations.length; i++) { + if (validators.active[delegations[i].delegation.validator_address]) { + const temp = { + label: + validators.active[delegations[i].delegation.validator_address] + .description.moniker, + value: { + shares: parseDelegation( + delegations[i], + chainInfo.config.currencies[0] + ), + validator: delegations[i].delegation.validator_address, + }, + }; + data.push(temp); + } else if ( + validators.inactive[delegations[i].delegation.validator_address] + ) { + const temp = { + label: + validators.inactive[delegations[i].delegation.validator_address] + .description.moniker, + value: { + shares: parseDelegation( + delegations[i], + chainInfo.config.currencies[0] + ), + validator: delegations[i].delegation.validator_address, + }, + }; + data.push(temp); + } + } + + setData(data); + }, [delegations]); + + const currency = chainInfo.config.currencies[0]; + + return ( + <> + ( + + )} + /> + ( + + option.value?.validator === value.value?.validator + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + { + return Number(value) > 0; + }, + }} + render={({ field, fieldState: { error } }) => ( + + {currency?.coinDenom} + + ), + }} + /> + )} + /> + + ); +} diff --git a/src/components/group/groupCmpStyles.ts b/src/components/group/groupCmpStyles.ts new file mode 100644 index 000000000..7329c55e4 --- /dev/null +++ b/src/components/group/groupCmpStyles.ts @@ -0,0 +1,21 @@ +export const gpStyles = { + alert_card: { + width: '100%', + mt: 2, + boxShadow: 'none' + }, + policy_box: { + border: '1px solid #eeeeee', + p: 5, + mt: 2, + borderRadius: 2, + width: '70%', + m: '0 auto', + + }, + flex_center: { display: 'flex', justifyContent: 'center' }, + f_22: { + fontSize: 22 + }, + j_center: {justifyContent: 'center', ml: 2} +} \ No newline at end of file diff --git a/src/features/common/commonSlice.js b/src/features/common/commonSlice.js index 562dfad43..0cb665579 100644 --- a/src/features/common/commonSlice.js +++ b/src/features/common/commonSlice.js @@ -7,7 +7,8 @@ const initialState = { }, txSuccess: { hash: '' - } + }, + txLoadRes: { load: false } } export const commonSlice = createSlice({ @@ -25,6 +26,12 @@ export const commonSlice = createSlice({ hash: action.payload.hash, } }, + setTxLoad: (state) => { + state.txLoadRes = { load: true } + }, + resetTxLoad: (state) => { + state.txLoadRes = { load: false } + }, resetTxHash: (state) => { state.txSuccess = { hash: '', @@ -35,10 +42,21 @@ export const commonSlice = createSlice({ message: '', type: '' } + }, + resetDecisionPolicies: (state) => { + state.groupPolicies = {} + }, + resetActiveProposals: (state)=>{ + state.policyProposals = {} } }, }) -export const { setError, resetError, setTxHash, resetTxHash } = commonSlice.actions +export const { + setError, resetError, + resetActiveProposals, + resetDecisionPolicies, + setTxLoad, resetTxLoad, + setTxHash, resetTxHash } = commonSlice.actions export default commonSlice.reducer \ No newline at end of file diff --git a/src/features/group/groupService.js b/src/features/group/groupService.js index 47cf97e3c..36328b9a3 100644 --- a/src/features/group/groupService.js +++ b/src/features/group/groupService.js @@ -1,14 +1,21 @@ import Axios from 'axios'; -import { convertPaginationToParams } from '../utils'; +import { convertPaginationToParams, convertPaginationToParamsOffset } from '../utils'; const groupByAdminURL = (admin) => `/cosmos/group/v1/groups_by_admin/${admin}` const groupByMemberURL = (address) => `/cosmos/group/v1/groups_by_member/${address}` +const groupMembersURL = groupId => `/cosmos/group/v1/group_members/${groupId}` +const groupByIdURL = groupId => `/cosmos/group/v1/group_info/${groupId}` +const groupMembersByIdURL = groupId => `/cosmos/group/v1/group_members/${groupId}` +const groupPoliciesByIdURL = groupId => `/cosmos/group/v1/group_policies_by_group/${groupId}` +const groupPolicyProposalsURL = address => `/cosmos/group/v1/proposals_by_group_policy/${address}` +const votesPropsalURL = proposal_id => `/cosmos/group/v1/votes_by_proposal/${proposal_id}` +const GroupProposalURL = proposal_id => `/cosmos/group/v1/proposal/${proposal_id}` const fetchGroupsByAdmin = (baseURL, admin, pagination) => { let uri = `${baseURL}${groupByAdminURL(admin)}` - const pageParams = convertPaginationToParams(pagination) - if (pageParams !== "") uri += `?${pageParams}` + const pageParams = convertPaginationToParamsOffset(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` return Axios.get(uri, { headers: { @@ -19,6 +26,18 @@ const fetchGroupsByAdmin = (baseURL, admin, pagination) => { const fetchGroupsByMember = (baseURL, address, pagination) => { let uri = `${baseURL}${groupByMemberURL(address)}` + const pageParams = convertPaginationToParamsOffset(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + +const fetchGroupMembers = (baseURL, groupId, pagination) => { + let uri = `${baseURL}${groupMembersURL(groupId)}` const pageParams = convertPaginationToParams(pagination) if (pageParams !== "") uri += `?${pageParams}` @@ -29,9 +48,87 @@ const fetchGroupsByMember = (baseURL, address, pagination) => { }) } +const fetchGroupById = (baseURL, groupId) => { + let uri = `${baseURL}${groupByIdURL(groupId)}` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + +const fetchGroupMembersById = (baseURL, groupId, pagination) => { + let uri = `${baseURL}${groupMembersByIdURL(groupId)}` + const pageParams = convertPaginationToParams(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + +const fetchVotesProposalById = (baseURL, proposalId, pagination) => { + let uri = `${baseURL}${votesPropsalURL(proposalId)}` + const pageParams = convertPaginationToParams(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + +const fetchGroupPoliciesById = (baseURL, groupId, pagination) => { + let uri = `${baseURL}${groupPoliciesByIdURL(groupId)}` + const pageParams = convertPaginationToParams(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + + +const fetchGroupPolicyProposalsById = (baseURL, address, pagination) => { + let uri = `${baseURL}${groupPolicyProposalsURL(address)}` + const pageParams = convertPaginationToParams(pagination) + if (pageParams !== "") uri += `?${pageParams}&pagination.count_total=true` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + +const fetchGroupProposalById = (baseURL, id) => { + let uri = `${baseURL}${GroupProposalURL(id)}` + + return Axios.get(uri, { + headers: { + Accept: 'application/json, text/plain, */*', + }, + }) +} + + + const result = { groupsByAdmin: fetchGroupsByAdmin, groupsByMember: fetchGroupsByMember, + groupMembers: fetchGroupMembers, + fetchGroupById: fetchGroupById, + fetchGroupMembersById: fetchGroupMembersById, + fetchGroupPoliciesById: fetchGroupPoliciesById, + fetchGroupPolicyProposalsById, + fetchVotesProposalById, + fetchGroupProposalById, } export default result; \ No newline at end of file diff --git a/src/features/group/groupSlice.js b/src/features/group/groupSlice.js index 129745834..f4bea5877 100644 --- a/src/features/group/groupSlice.js +++ b/src/features/group/groupSlice.js @@ -1,10 +1,11 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { fee, signAndBroadcastProto } from "../../txns/execute"; -import { CreateGroup, CreateGroupWithPolicy } from "../../txns/proto"; -import { setError, setTxHash } from "../common/commonSlice"; +import { fee, signAndBroadcastAddGroupPolicy, signAndBroadcastGroupMsg, signAndBroadcastGroupProposal, signAndBroadcastGroupProposalExecute, signAndBroadcastGroupProposalVote, signAndBroadcastLeaveGroup, signAndBroadcastProto, signAndBroadcastUpdateGroupAdmin, signAndBroadcastUpdateGroupMembers, signAndBroadcastUpdateGroupMetadata, signAndBroadcastUpdateGroupPolicy, signAndBroadcastUpdateGroupPolicyAdmin, signAndBroadcastUpdateGroupPolicyMetadata } from "../../txns/execute"; +import { CreateGroup, CreateGroupPolicy, CreateGroupProposal, CreateGroupWithPolicy, CreateLeaveGroupMember, CreateProposalExecute, CreateProposalVote, UpdateGroupAdmin, UpdateGroupMembers, UpdateGroupMetadata, UpdateGroupPolicy, UpdatePolicyAdmin, UpdatePolicyMetadata } from "../../txns/group/group"; +import { resetDecisionPolicies, resetTxLoad, setError, setTxHash, setTxLoad } from "../common/commonSlice"; import groupService from "./groupService"; const initialState = { + txCreateGroupRes: {}, tx: { status: "idle", type: "", @@ -21,57 +22,838 @@ const initialState = { status: "idle", }, }, + members: { + data: [] + }, + groupInfo: { + }, + groupMembers: {}, + groupPolicies: {}, + groupProposalRes: {}, + proposals: {}, + voteRes: {}, + executeRes: {}, + updateGroupRes: {}, + leaveGroupRes: {}, + proposalVotes: { + data: [] + }, + groupProposal: {}, + updateGroupPolicyRes: {}, + updateGroupAdminRes: {}, + updateGroupMetadataRes: {}, + addGroupPolicyRes: {}, + addPolicyMetadataRes: {}, + updatePolicyAdminRes: {}, + policyProposals: {}, }; -export const getGroupsByAdmin = createAsyncThunk( - "group/group-by-admin", - async (data) => { - const response = await groupService.groupsByAdmin( - data.baseURL, - data.admin, - data.pagination - ); - return response.data; +export const getGroupsByAdmin = createAsyncThunk( + "group/group-by-admin", + async (data) => { + const response = await groupService.groupsByAdmin( + data.baseURL, + data.admin, + data.pagination + ); + return response.data; + } +); + +export const getGroupById = createAsyncThunk( + "group/group-by-id", + async (data) => { + const response = await groupService.fetchGroupById( + data.baseURL, data.id + ); + return response.data; + } +); + +export const getGroupMembersById = createAsyncThunk( + "group/group-members-by-id", + async (data) => { + const response = await groupService.fetchGroupMembersById( + data.baseURL, data.id, data.pagination + ); + return response.data; + } +); + +export const getVotesProposalById = createAsyncThunk( + "group/group-proposal-votes", + async (data) => { + const response = await groupService.fetchVotesProposalById( + data.baseURL, data.id, data.pagination + ); + return response.data; + } +); + +export const getGroupProposalById = createAsyncThunk( + "group/group-proposal-info", + async (data) => { + const response = await groupService.fetchGroupProposalById( + data.baseURL, data.id + ); + return response.data; + } +); + +export const getGroupPoliciesById = createAsyncThunk( + "group/group-policies-by-id", + async (data, { dispatch }) => { + // dispatch(resetDecisionPolicies()) + const response = await groupService.fetchGroupPoliciesById( + data.baseURL, data.id, data.pagination + ); + return response.data; + } +); + +export const getGroupsByMember = createAsyncThunk( + "group/group-by-member", + async (data) => { + const response = await groupService.groupsByMember( + data.baseURL, + data.address, + data.pagination + ); + return response.data; + } +); + +export const getGroupMembers = createAsyncThunk( + "group/get-group-members", + async (data) => { + const response = await groupService.groupMembers(data.baseURL, data.groupId); + return response.data; + } +) + +export const getGroupPolicyProposalsByPage = createAsyncThunk( + "group/get-group-policy-proposals-by-page", + async (data, { dispatch }) => { + var totalData = []; + var allPolicies = []; + + try { + allPolicies = await groupService.fetchGroupPoliciesById( + data.baseURL, data.groupId, data?.pagination + ) + } catch (error) { + throw error; + } + + const getProposalsPagination = async (address, pagination) => { + const response = await groupService.fetchGroupPolicyProposalsById( + data.baseURL, address, + pagination + ); + + let filteredProposals = response?.data?.proposals?.filter(p => p?.status === 'PROPOSAL_STATUS_SUBMITTED') + + totalData = [...totalData, ...filteredProposals]; + + if (response?.data?.pagination?.next_key) + return getProposalsPagination(address, { limit: 1, key: response?.data?.pagination?.next_key }) + else return totalData; + } + + + if (allPolicies?.data?.group_policies?.length) { + let data = allPolicies?.data?.group_policies || []; + let totalPolicyData = []; + for (let i = 0; i < data.length; i++) { + + try { + await getProposalsPagination(data[i]?.address, { + limit: 1, + key: '' + }); + + } catch (error) { + console.log('Error while getting proposals', error) + throw error; + } + } + + return totalData; + } else return '' + } +) + +export const getGroupPolicyProposals = createAsyncThunk( + "group/get-group-policy-proposals", + async (data, { dispatch }) => { + const response = await groupService.fetchGroupPolicyProposalsById( + data.baseURL, data.address, + data.pagination + ); + + return response.data; + } +) + +export const txGroupProposalVote = createAsyncThunk( + 'group/tx-group-proposal-vote', + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + + try { + let msg = CreateProposalVote( + data.proposalId, + data.voter, + data.option, + data?.metadata || '', + ) + + console.log('msg----', msg) + + const result = await signAndBroadcastGroupProposalVote( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating propsoal', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group proposal', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +) + +export const txGroupProposalExecute = createAsyncThunk( + 'group/tx-group-proposal-execute', + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + try { + let msg = CreateProposalExecute( + data.proposalId, + data.executor + ) + + console.log('msg----', msg) + + const result = await signAndBroadcastGroupProposalExecute( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating propsoal', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group proposal', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +) + +export const txUpdateGroupAdmin = createAsyncThunk( + 'group/tx-update-group-admin', + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + + try { + let msg = UpdateGroupAdmin( + data.admin, + data.groupId, + data.newAdmin + ) + + console.log('msg----', msg) + + const result = await signAndBroadcastUpdateGroupAdmin( + data.signer, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + dispatch(resetTxLoad()); + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating propsoal', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group proposal', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +) + +export const txUpdateGroupMetadata = createAsyncThunk( + 'group/tx-update-group-metadata', + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + + try { + let msg = UpdateGroupMetadata( + data.admin, + data.groupId, + data.metadata + ) + + console.log('msg----', msg) + + const result = await signAndBroadcastUpdateGroupMetadata( + data.signer, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + dispatch(resetTxLoad()); + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating propsoal', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group proposal', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +) + +export const txCreateGroupProposal = createAsyncThunk( + 'group/tx-create-group-proposal', + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + console.log('proposal --- ', data) + try { + let msg = CreateGroupProposal( + data.groupPolicyAddress, + data.proposers, + data.metadata, + data.messages, + ) + + console.log('msg----', msg) + + const result = await signAndBroadcastGroupProposal( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + dispatch(resetTxLoad()); + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating propsoal', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group proposal', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +) + +export const txCreateGroup = createAsyncThunk( + "group/tx-create-group", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + dispatch(setTxLoad()); + let msg; + try { + if (data?.members?.length > 0) { + if (data?.policyData && Object.keys(data?.policyData)?.length) { + + msg = CreateGroupWithPolicy( + data.admin, + data.groupMetaData, + data.members, + data.decisionPolicy, + data.policyData, + data.policyAsAdmin + ); + } else { + msg = CreateGroup(data.admin, data.groupMetaData, data?.members); + } + } else { + msg = CreateGroup(data.admin, data.groupMetaData, []); + } + + console.log('msg--', msg) + console.log('admin--', data) + + const result = await signAndBroadcastGroupMsg( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +); + +export const txUpdateGroupMember = createAsyncThunk( + "group/tx-update-group-member", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + let msg; + dispatch(setTxLoad()); + console.log({ data }) + try { + msg = UpdateGroupMembers( + data.admin, + data.members, + data.groupId + ); + + console.log({ msg }) + + const result = await signAndBroadcastUpdateGroupMembers( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating the group', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +); + +export const txAddGroupPolicy = createAsyncThunk( + "group/tx-add-group-policy", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + let msg; + dispatch(setTxLoad()); + console.log({ data }) + try { + msg = CreateGroupPolicy( + data.admin, + data.groupId, + data.policyMetadata + ); + + console.log({ msg }) + + const result = await signAndBroadcastAddGroupPolicy( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while creating the group', result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +); + +export const txUpdateGroupPolicy = createAsyncThunk( + "group/tx-update-group-policy", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + let msg; + dispatch(setTxLoad()); + console.log({ data }) + try { + msg = UpdateGroupPolicy( + data.admin, + data.groupPolicyAddress, + data.policyMetadata + ); + + console.log({ msg }) + + const result = await signAndBroadcastUpdateGroupPolicy( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while updating the policy metadata', result, result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } } ); -export const getGroupsByMember = createAsyncThunk( - "group/group-by-member", - async (data) => { - const response = await groupService.groupsByMember( - data.baseURL, - data.address, - data.pagination - ); - return response.data; +export const txUpdateGroupPolicyMetdata = createAsyncThunk( + "group/tx-update-group-policy-metadata", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + let msg; + dispatch(setTxLoad()); + console.log({ data }) + try { + msg = UpdatePolicyMetadata( + data.admin, + data.groupPolicyAddress, + data.metadata + ); + + console.log({ msg }) + + const result = await signAndBroadcastUpdateGroupPolicyMetadata( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + console.log('Error while updating the policy metadata', result, result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } } ); -export const txCreateGroup = createAsyncThunk( - "group/tx-create-group", +export const txUpdateGroupPolicyAdmin = createAsyncThunk( + "group/tx-update-group-policy-admin", async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { let msg; + dispatch(setTxLoad()); + console.log({ data }) try { - if (data.members.length > 0) { - if (data.decisionPolicy) { - msg = CreateGroupWithPolicy( - data.admin, - data.groupMetadata, - data.members, - data.decisionPolicy, - data.policyMetadata, - data.policyAsAdmin - ); - } - msg = CreateGroup(data.admin, data.groupMetadata, data.members); + msg = UpdatePolicyAdmin( + data.admin, + data.groupPolicyAddress, + data.newAdmin + ); + + console.log({ msg }) + + const result = await signAndBroadcastUpdateGroupPolicyAdmin( + data.admin, + [msg], + fee(data.denom, data.feeAmount, 260000), + data.chainId, + data.rpc + ); + + dispatch(resetTxLoad()); + + if (result?.code === 0) { + dispatch( + setTxHash({ + hash: result?.transactionHash, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { - msg = CreateGroup(data.admin, data.groupMetadata, []); + console.log('Error while updating the policy metadata', result, result?.rawLog) + dispatch( + setError({ + type: "error", + message: result?.rawLog, + }) + ); + return rejectWithValue(result?.rawLog); } - const result = await signAndBroadcastProto( + } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) + dispatch( + setError({ + type: "error", + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +); + +export const txLeaveGroupMember = createAsyncThunk( + "group/tx-leave-group-member", + async (data, { rejectWithValue, fulfillWithValue, dispatch }) => { + let msg; + dispatch(setTxLoad()); + console.log({ data }) + try { + msg = CreateLeaveGroupMember( + data.admin, + data.groupId + ); + + const result = await signAndBroadcastLeaveGroup( + data.admin, [msg], fee(data.denom, data.feeAmount, 260000), + data.chainId, data.rpc ); + + dispatch(resetTxLoad()); + if (result?.code === 0) { dispatch( setTxHash({ @@ -80,6 +862,7 @@ export const txCreateGroup = createAsyncThunk( ); return fulfillWithValue({ txHash: result?.transactionHash }); } else { + console.log('Error while creating the group', result?.rawLog) dispatch( setError({ type: "error", @@ -89,6 +872,10 @@ export const txCreateGroup = createAsyncThunk( return rejectWithValue(result?.rawLog); } } catch (error) { + + dispatch(resetTxLoad()); + + console.log('Error while creating the group', error.message) dispatch( setError({ type: "error", @@ -107,6 +894,16 @@ export const groupSlice = createSlice({ resetTxType: (state, _) => { state.tx.type = ""; }, + resetGroupTx: (state, _) => { + state.tx.status = ''; + state.txCreateGroupRes = {}; + }, + resetUpdateGroupMember: (state) => { + state.updateGroupRes.status = ''; + }, + resetCreateGroupProposalRes: state => { + state.groupProposalRes.status = ''; + } }, // The `extraReducers` field lets the slice handle actions defined elsewhere, // including actions generated by createAsyncThunk or in other slices. @@ -146,14 +943,240 @@ export const groupSlice = createSlice({ builder .addCase(txCreateGroup.pending, (state) => { state.tx.status = `pending`; + state.txCreateGroupRes.status = 'pending'; }) .addCase(txCreateGroup.fulfilled, (state, _) => { state.tx.status = `idle`; + state.txCreateGroupRes.status = 'idle'; }) .addCase(txCreateGroup.rejected, (state, _) => { state.tx.status = `rejected`; + state.txCreateGroupRes.status = 'rejected'; + }); + + builder + .addCase(getGroupMembers.pending, (state) => { + state.members.status = `pending`; + }) + .addCase(getGroupMembers.fulfilled, (state, action) => { + state.members.status = 'idle'; + console.log('action paydddddddd', action.payload) + state.members.data = [...state.members.data, action.payload] + }) + .addCase(getGroupMembers.rejected, (state, _) => { + state.members.status = `rejected`; + }); + + builder + .addCase(getGroupById.pending, (state) => { + state.groupInfo.status = `pending`; + }) + .addCase(getGroupById.fulfilled, (state, action) => { + console.log('fffffffff', action.payload) + state.groupInfo.status = 'idle'; + state.groupInfo.data = action.payload + }) + .addCase(getGroupById.rejected, (state, _) => { + state.groupInfo.status = `rejected`; + }); + + builder + .addCase(getGroupMembersById.pending, (state) => { + state.groupMembers.status = `pending`; + }) + .addCase(getGroupMembersById.fulfilled, (state, action) => { + state.groupMembers.status = 'idle'; + state.groupMembers.data = action.payload + }) + .addCase(getGroupMembersById.rejected, (state, _) => { + state.groupMembers.status = `rejected`; + }); + + builder + .addCase(getGroupPoliciesById.pending, (state) => { + state.groupPolicies.status = `pending`; + }) + .addCase(getGroupPoliciesById.fulfilled, (state, action) => { + state.groupPolicies.status = 'idle'; + state.groupPolicies.data = action.payload + }) + .addCase(getGroupPoliciesById.rejected, (state, _) => { + state.groupPolicies.status = `rejected`; + }); + + builder + .addCase(getVotesProposalById.pending, (state) => { + state.proposalVotes.status = `pending`; + }) + .addCase(getVotesProposalById.fulfilled, (state, action) => { + state.proposalVotes.status = 'idle'; + state.proposalVotes.data = action.payload + }) + .addCase(getVotesProposalById.rejected, (state, _) => { + state.proposalVotes.status = `rejected`; + }); + + builder + .addCase(getGroupPolicyProposals.pending, (state) => { + state.proposals.status = `pending`; + }) + .addCase(getGroupPolicyProposals.fulfilled, (state, action) => { + state.proposals.status = 'idle'; + state.proposals.data = action.payload + }) + .addCase(getGroupPolicyProposals.rejected, (state, _) => { + state.proposals.status = `rejected`; + }); + + builder + .addCase(txCreateGroupProposal.pending, (state) => { + state.groupProposalRes.status = `pending`; + }) + .addCase(txCreateGroupProposal.fulfilled, (state, action) => { + state.groupProposalRes.status = 'idle'; + }) + .addCase(txCreateGroupProposal.rejected, (state, _) => { + state.groupProposalRes.status = `rejected`; + }); + + builder + .addCase(txGroupProposalVote.pending, (state) => { + state.voteRes.status = `pending`; + }) + .addCase(txGroupProposalVote.fulfilled, (state, action) => { + state.voteRes.status = 'idle'; + }) + .addCase(txGroupProposalVote.rejected, (state, _) => { + state.voteRes.status = `rejected`; + }); + + builder + .addCase(txGroupProposalExecute.pending, (state) => { + state.executeRes.status = `pending`; + }) + .addCase(txGroupProposalExecute.fulfilled, (state, action) => { + state.executeRes.status = 'idle'; + }) + .addCase(txGroupProposalExecute.rejected, (state, _) => { + state.executeRes.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupMember.pending, (state) => { + state.updateGroupRes.status = `pending`; + }) + .addCase(txUpdateGroupMember.fulfilled, (state, action) => { + state.updateGroupRes.status = 'idle'; + }) + .addCase(txUpdateGroupMember.rejected, (state, _) => { + state.updateGroupRes.status = `rejected`; + }); + + builder + .addCase(txLeaveGroupMember.pending, (state) => { + state.leaveGroupRes.status = `pending`; + }) + .addCase(txLeaveGroupMember.fulfilled, (state, action) => { + state.leaveGroupRes.status = 'idle'; + }) + .addCase(txLeaveGroupMember.rejected, (state, _) => { + state.leaveGroupRes.status = `rejected`; + }); + + builder + .addCase(getGroupProposalById.pending, (state) => { + state.groupProposal.status = `pending`; + }) + .addCase(getGroupProposalById.fulfilled, (state, action) => { + state.groupProposal.status = 'idle'; + state.groupProposal.data = action.payload; + }) + .addCase(getGroupProposalById.rejected, (state, _) => { + state.groupProposal.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupPolicy.pending, (state) => { + state.updateGroupPolicyRes.status = `pending`; + }) + .addCase(txUpdateGroupPolicy.fulfilled, (state, action) => { + state.updateGroupPolicyRes.status = 'idle'; + }) + .addCase(txUpdateGroupPolicy.rejected, (state, _) => { + state.updateGroupPolicyRes.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupAdmin.pending, (state) => { + state.updateGroupAdminRes.status = `pending`; + }) + .addCase(txUpdateGroupAdmin.fulfilled, (state, action) => { + state.updateGroupAdminRes.status = 'idle'; + }) + .addCase(txUpdateGroupAdmin.rejected, (state, _) => { + state.updateGroupAdminRes.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupMetadata.pending, (state) => { + state.updateGroupMetadataRes.status = `pending`; + }) + .addCase(txUpdateGroupMetadata.fulfilled, (state, action) => { + state.updateGroupMetadataRes.status = 'idle'; + }) + .addCase(txUpdateGroupMetadata.rejected, (state, _) => { + state.updateGroupMetadataRes.status = `rejected`; + }); + + builder + .addCase(txAddGroupPolicy.pending, (state) => { + state.addGroupPolicyRes.status = `pending`; + }) + .addCase(txAddGroupPolicy.fulfilled, (state, action) => { + state.addGroupPolicyRes.status = 'idle'; + }) + .addCase(txAddGroupPolicy.rejected, (state, _) => { + state.addGroupPolicyRes.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupPolicyMetdata.pending, (state) => { + state.updateGroupMetadataRes.status = `pending`; + }) + .addCase(txUpdateGroupPolicyMetdata.fulfilled, (state, action) => { + state.updateGroupMetadataRes.status = 'idle'; + }) + .addCase(txUpdateGroupPolicyMetdata.rejected, (state, _) => { + state.updateGroupMetadataRes.status = `rejected`; + }); + + builder + .addCase(txUpdateGroupPolicyAdmin.pending, (state) => { + state.updatePolicyAdminRes.status = `pending`; + }) + .addCase(txUpdateGroupPolicyAdmin.fulfilled, (state, action) => { + state.updatePolicyAdminRes.status = 'idle'; + }) + .addCase(txUpdateGroupPolicyAdmin.rejected, (state, _) => { + state.updatePolicyAdminRes.status = `rejected`; + }); + + builder + .addCase(getGroupPolicyProposalsByPage.pending, (state) => { + state.policyProposals.status = `pending`; + }) + .addCase(getGroupPolicyProposalsByPage.fulfilled, (state, action) => { + state.policyProposals.status = 'idle'; + state.policyProposals.data = action?.payload; + }) + .addCase(getGroupPolicyProposalsByPage.rejected, (state, _) => { + state.policyProposals.status = `rejected`; }); }, }); +export const { resetGroupTx, + resetCreateGroupProposalRes, + resetUpdateGroupMember } = groupSlice.actions; + export default groupSlice.reducer; diff --git a/src/features/node/nodeService.js b/src/features/node/nodeService.js new file mode 100644 index 000000000..6edb025ef --- /dev/null +++ b/src/features/node/nodeService.js @@ -0,0 +1,16 @@ +import Axios from "axios"; + +const NODE_STATUS_URL = "/cosmos/base/tendermint/v1beta1/node_info"; + +const fetchNodeInfo = (baseURL) => + Axios.get(`${baseURL}${NODE_STATUS_URL}`, { + headers: { + Accept: "application/json, text/plain, */*", + }, + }); + +const result = { + fetchNodeInfo +}; + +export default result; diff --git a/src/features/node/nodeSlice.js b/src/features/node/nodeSlice.js new file mode 100644 index 000000000..c10d3dea0 --- /dev/null +++ b/src/features/node/nodeSlice.js @@ -0,0 +1,42 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import nodeService from "./nodeService"; + +const initialState = { + nodeInfo: {} +}; + +export const getNodeInfo = createAsyncThunk( + "node/node-info", + async (data, { rejectWithValue }) => { + try { + const response = await nodeService.fetchNodeInfo(data.baseURL); + return response.data; + } catch (error) { + return rejectWithValue(error); + } + } +); + + +export const nodeSlice = createSlice({ + name: "node", + initialState, + reducers: { + }, + extraReducers: (builder) => { + builder + .addCase(getNodeInfo.pending, (state) => { + state.nodeInfo.status = 'pending' + }) + .addCase(getNodeInfo.fulfilled, (state, action) => { + state.nodeInfo.status = 'idle' + state.nodeInfo.data = action?.payload; + }) + .addCase(getNodeInfo.rejected, (state, action) => { + state.nodeInfo.status = 'rejected' + }); + }, +}); + +export const { } = nodeSlice.actions; +export default nodeSlice.reducer; diff --git a/src/features/utils.js b/src/features/utils.js index 6f22eb215..6482ab580 100644 --- a/src/features/utils.js +++ b/src/features/utils.js @@ -1,20 +1,42 @@ export const convertPaginationToParams = (pagination) => { - let result = ""; - if (pagination === undefined || (pagination?.key === null && pagination?.limit === null) || - (pagination?.key === undefined && pagination?.limit === undefined) - ) { - return "" - } - if (pagination.key !== null) { - result += `pagination.key=${encodeURIComponent(pagination.key)}` - if (pagination.limit !== null) { - result += `&pagination.limit=${pagination.limit}` - } - } else { - if (pagination.limit !== null) { - result += `pagination.limit=${pagination.limit}` - } - } + let result = ""; + if (pagination === undefined || (pagination?.key === null && pagination?.limit === null) || + (pagination?.key === undefined && pagination?.limit === undefined) + ) { + return "" + } + if (pagination.key !== null) { + result += `pagination.key=${encodeURIComponent(pagination.key)}` + if (pagination.limit !== null) { + result += `&pagination.limit=${pagination.limit}` + } + } else { + if (pagination.limit !== null) { + result += `pagination.limit=${pagination.limit}` + } + } - return result + return result +} + +export const convertPaginationToParamsOffset = (pagination) => { + let result = ""; + if (pagination === undefined || (pagination?.offset === null && + pagination?.limit === null) || + (pagination?.offset === undefined && pagination?.limit === undefined) + ) { + return "" + } + if (pagination.offset !== null) { + result += `pagination.offset=${pagination.offset}` + if (pagination.limit !== null) { + result += `&pagination.limit=${pagination.limit}` + } + } else { + if (pagination.limit !== null) { + result += `pagination.limit=${pagination.limit}` + } + } + + return result } \ No newline at end of file diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js index 5145de130..817f4703e 100644 --- a/src/pages/Dashboard.js +++ b/src/pages/Dashboard.js @@ -18,6 +18,7 @@ import AlertTitle from "@mui/material/AlertTitle"; import Snackbar from "@mui/material/Snackbar"; import Overview from "./Overview"; import { CustomAppBar } from "../components/CustomAppBar"; +import { resetError, resetTxLoad, setError } from "../features/common/commonSlice"; import Page404 from "./Page404"; import AppDrawer from "../components/AppDrawer"; import { Alert } from "../components/Alert"; @@ -27,6 +28,13 @@ import { Paper, Typography } from "@mui/material"; import { exitAuthzMode } from "../features/authz/authzSlice"; import { copyToClipboard } from "../utils/clipboard"; +const GroupPage = lazy(() => import("./GroupPage")); +const Group = lazy(() => import("./group/Group")); +const Policy = lazy(() => import("./group/Policy")); +const CreateGroupPage = lazy(() => import("./group/CreateGroup")); +const Proposal = lazy(() => import("./group/Proposal")) +const CreateProposal = lazy(() => import("./group/CreateProposal")) + const Authz = lazy(() => import("./Authz")); const Validators = lazy(() => import("./Validators")); const Proposals = lazy(() => import("./Proposals")); @@ -51,6 +59,7 @@ function DashboardContent(props) { setDarkMode(!darkMode); }; + const txLoadRes = useSelector(state => state?.common?.txLoadRes?.load) const [pallet, setPallet] = useState(getPallet()); const balance = useSelector((state) => state.bank.balance); @@ -310,11 +319,44 @@ function DashboardContent(props) { } > - {/* }> + }> + + + }> + }> + + + + }> + }> + + + + }> + }> + + + }> + }> + + + + }> } - > */} + element={ + }> + + + } + > + }> @@ -352,6 +394,33 @@ function DashboardContent(props) { <> )} + { + showTxSnack(false); + }} + anchorOrigin={{ vertical: "top", horizontal: "right" }} + > + , + }} + onClose={() => { + dispatch(resetTxLoad()) + }} + severity="info" + sx={{ width: "100%" }} + > + + Loading.. + + + Please wait for sometime. + + + import('./group/AdminGroupList')) +const MemberGroupList = lazy(() => import('./group/MemberGroupList')) - const address = useSelector((state) => state.wallet.address); - const chainInfo = useSelector((state) => state.wallet.chainInfo); - const groups = useSelector((state) => state.group.groups); - const dispatch = useDispatch(); - useEffect(() => { - if (address.length > 0) - dispatch( - getGroupsByAdmin({ - baseURL: chainInfo.config.rest, - admin: address, - }) - ); - }, [address]); +export default function GroupPage() { + const [tab, setTab] = useState(0); - useEffect(() => { - if (address.length > 0 && selectedTab === "member") - dispatch( - getGroupsByMember({ - baseURL: chainInfo.config.rest, - address: address, - }) - ); - }, [selectedTab]); + const handleTabChange = (value) => { + setTab(value); + } const navigate = useNavigate(); function navigateTo(path) { @@ -47,10 +27,7 @@ export default function GroupPage() { - - - - - - - {selectedTab === "admin" ? ( - { - console.log(group); - }} - /> - ) : ( - { - console.log(group); - }} - /> - )} + + + + + + }> + + + + + + }> + + + - + ); } diff --git a/src/pages/group/ActiveProposals.jsx b/src/pages/group/ActiveProposals.jsx new file mode 100644 index 000000000..58bde33d2 --- /dev/null +++ b/src/pages/group/ActiveProposals.jsx @@ -0,0 +1,75 @@ +import { Box, CircularProgress, Grid } from '@mui/material' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import AlertMsg from '../../components/group/AlertMsg'; +import ProposalCard from '../../components/group/ProposalCard'; +import { resetActiveProposals } from '../../features/common/commonSlice'; +import { getGroupPoliciesById, getGroupPolicyProposals, getGroupPolicyProposalsByPage } from '../../features/group/groupSlice'; + +const limit = 100; + +function ActiveProposals({ id, wallet }) { + const dispatch = useDispatch(); + + var [proposals, setProposals] = useState([]); + + const proposalsRes = useSelector(state => state.group?.policyProposals) + console.log('proposa ---res', proposalsRes) + + const getProposalByAddress = () => { + dispatch(getGroupPolicyProposalsByPage({ + baseURL: wallet?.chainInfo?.config?.rest, + groupId: id, + })) + } + + useEffect(() => { + getProposalByAddress(); + }, []) + + useEffect(() => { + setProposals([]) + if (proposalsRes?.status === 'idle') { + + // let allProposals = proposalsRes?.data?.filter(p => p.status === 'PROPOSAL_STATUS_SUBMITTED') + // let allProposals = proposalsRes?.data?.sort((a, b) => b.id - a.id) + // proposals = [...proposals, ...allProposals] + setProposals([...proposalsRes?.data]) + } + + }, [proposalsRes?.status]) + + useEffect(() => { + return () => { + // setProposals([]) + dispatch(resetActiveProposals()) + } + }) + + return ( + + + { + !proposals?.length ? + : null + + } + + { + proposalsRes?.status === 'pending' ? + : null + } + + { + proposals?.map(p => ( + + + + )) + } + + + ) +} + +export default ActiveProposals \ No newline at end of file diff --git a/src/pages/group/AddFileTx.jsx b/src/pages/group/AddFileTx.jsx new file mode 100644 index 000000000..973cef154 --- /dev/null +++ b/src/pages/group/AddFileTx.jsx @@ -0,0 +1,566 @@ +import { + Box, FormControl, Grid, InputLabel, + MenuItem, Pagination, Select, Typography, + IconButton, + Button, + TextField, +} from '@mui/material' +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Divider } from "@mui/material"; +import { useForm, Controller, FormProvider, useFormContext } from "react-hook-form"; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; + +import FileProposalOptions from '../../components/group/FileProposalOptions'; +import { + DELEGATE_TYPE_URL, + parseDelegateMsgsFromContent, + parseReDelegateMsgsFromContent, + parseSendMsgsFromContent, + parseUnDelegateMsgsFromContent, + REDELEGATE_TYPE_URL, + SEND_TYPE_URL, + UNDELEGATE_TYPE_URL, +} from "./utils"; + +import { setError } from '../../features/common/commonSlice'; +import TxBasicFields from '../../components/group/TxBasicFields'; +import { parseBalance } from "../../utils/denom"; +import { shortenAddress } from '../../utils/util'; +import { useNavigate, useParams } from 'react-router-dom'; +import { resetCreateGroupProposalRes, txCreateGroupProposal } from '../../features/group/groupSlice'; + +const TYPE_SEND = "SEND"; +const TYPE_DELEGATE = "DELEGATE"; +const TYPE_UNDELEGATE = "UNDELEGATE"; +const TYPE_REDELEGATE = "REDELEGATE"; + + +const PER_PAGE = 6; + +function AddFileTx({ address }) { + const { policyAddress, id } = useParams(); + + const [txType, setTxType] = useState(); + const [messages, setMessages] = useState([]); + const [slicedMsgs, setSlicedMsgs] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + + const dispatch = useDispatch(); + + const wallet = useSelector(state => state?.wallet); + const { chainInfo } = wallet; + + const methods = useForm({ + defaultValues: { + gas: 20000 + } + }); + const { setValue, control, handleSubmit } = methods; + + var createRes = useSelector((state) => state.group.groupProposalRes); + + let navigate = useNavigate(); + + useEffect(() => { + if (createRes?.status === "rejected") { + dispatch( + setError({ + type: "error", + message: createRes?.error, + }) + ); + } else if (createRes?.status === "idle") { + dispatch( + setError({ + type: "success", + message: "Transaction created", + }) + ); + + setTimeout(() => { + navigate(`/groups/${id}/policies/${policyAddress}`); + }, 200); + } + }, [createRes?.status]); + + useEffect(() => { + return () => { + dispatch(resetCreateGroupProposalRes()) + } + }, []) + + useEffect(() => { + if (messages.length < PER_PAGE) { + setSlicedMsgs(messages); + } else { + setCurrentPage(1); + setSlicedMsgs(messages?.slice(0, 1 * PER_PAGE)); + } + }, [messages]); + + const onDeleteMsg = (index) => { + const arr = messages.filter((_, i) => i !== index); + setMessages(arr); + setValue('msgs', arr) + }; + + const onFileContents = (content, type) => { + switch (type) { + case TYPE_SEND: { + const [parsedTxns, error] = parseSendMsgsFromContent(policyAddress, content); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + methods.setValue('msgs', parsedTxns) + } + break; + } + case TYPE_DELEGATE: { + const [parsedTxns, error] = parseDelegateMsgsFromContent( + policyAddress, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + methods.setValue('msgs', parsedTxns) + } + break; + } + case TYPE_REDELEGATE: { + const [parsedTxns, error] = parseReDelegateMsgsFromContent( + policyAddress, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + methods.setValue('msgs', parsedTxns) + } + break; + } + case TYPE_UNDELEGATE: { + const [parsedTxns, error] = parseUnDelegateMsgsFromContent( + policyAddress, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + methods.setValue('msgs', parsedTxns); + } + break; + } + default: + setMessages([]); + methods.setValue('msgs', []) + } + }; + + const renderMessage = (msg, index, currency, onDelete) => { + switch (msg.typeUrl) { + case SEND_TYPE_URL: { + return RenderSendMessage(msg, index, currency, onDelete); + } + case DELEGATE_TYPE_URL: + return RenderDelegateMessage(msg, index, currency, onDelete); + case UNDELEGATE_TYPE_URL: + return RenderUnDelegateMessage(msg, index, currency, onDelete); + case REDELEGATE_TYPE_URL: + return RenderReDelegateMessage(msg, index, currency, onDelete); + default: + return ""; + } + }; + + const onSubmit = (data) => { + dispatch(txCreateGroupProposal( + { + metadata: data?.metadata, + admin: wallet?.address, + proposers: [wallet?.address], + messages: data?.msgs, + groupPolicyAddress: policyAddress, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config.currencies[0].coinMinimalDenom, + feeAmount: data?.fees, + memo: data?.memo, + gas: data?.gas + } + )) + }; + + return ( + + + + + + Select Transaction + + + + + {txType && || null} + + + + + Messages + + + + + +
+ {messages.length === 0 ? ( + + No Messages + + ) : null} + {slicedMsgs.map((msg, index) => { + return ( + + {renderMessage( + msg, + index + PER_PAGE * (currentPage - 1), + chainInfo.config.currencies[0], + onDeleteMsg + )} + + + ); + })} + + {messages.length > 6 ? ( + { + setCurrentPage(v); + setSlicedMsgs( + messages?.slice((v - 1) * PER_PAGE, v * PER_PAGE) + ); + }} + /> + ) : null} + + + + {messages.length > 0 ? ( + <> + ( + + )} + /> + + + + ) : null} + +
+
+
+
+ +
+ ) +} + +export default AddFileTx; + +export const RenderSendMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Send  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + to  + + + {shortenAddress(message.value.toAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Delegate  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + to  + + + {shortenAddress(message.value.validatorAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderUnDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Undelegate  + + + {parseBalance( + [message.value.amount], + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + from  + + + {shortenAddress(message.value?.validatorAddress || "", 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderReDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Redelegate  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + from  + + + {shortenAddress(message.value.validatorSrcAddress, 21)}  + + + to  + + + {shortenAddress(message.value.validatorDstAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; \ No newline at end of file diff --git a/src/pages/group/AddManualTx.jsx b/src/pages/group/AddManualTx.jsx new file mode 100644 index 000000000..7a1e99555 --- /dev/null +++ b/src/pages/group/AddManualTx.jsx @@ -0,0 +1,255 @@ +import { Box, Button, FormControl, InputLabel, MenuItem, Select, TextField, Typography } from '@mui/material' +import React, { useEffect, useState } from 'react' +import Delegate from '../../components/group/bulk/Delegate'; +import RedelegateForm from '../../components/group/bulk/RedelegateForm'; +import Send from '../../components/group/bulk/Send'; +import UnDelegateForm from '../../components/group/bulk/UnDelegateForm'; +import TxBasicFields from '../../components/group/TxBasicFields'; +import { useForm, FormProvider, useFormContext, Controller } from "react-hook-form"; +import { Decimal } from "@cosmjs/math"; +import { getAllValidators, getDelegations } from '../../features/staking/stakeSlice'; +import { useDispatch, useSelector } from 'react-redux'; +import { resetCreateGroupProposalRes, txCreateGroupProposal } from '../../features/group/groupSlice'; +import { fee } from '../../txns/execute'; +import { useNavigate, useParams } from 'react-router-dom'; +import { setError } from '../../features/common/commonSlice'; + +const TYPE_SEND = "SEND"; +const TYPE_DELEGATE = "DELEGATE"; +const TYPE_UNDELEGATE = "UNDELEGATE"; +const TYPE_REDELEGATE = "REDELEGATE"; + +const getAmountInAtomics = (amount, currency) => { + const amountInAtomics = Decimal.fromUserInput( + amount, + Number(currency.coinDecimals) + ).atomics; + + return { + amount: amountInAtomics, + denom: currency.coinMinimalDenom, + } +} + +function AddManualTx({ + address, + chainInfo, + handleCancel +}) { + const { policyAddress, id } = useParams(); + + const currency = chainInfo?.config?.currencies[0]; + + const [txType, setTxType] = useState(); + + const methods = useForm({ + defaultValues: { + gas: 20000 + } + }); + const dispatch = useDispatch(); + + const validators = useSelector((state) => state.staking.validators); + const wallet = useSelector(state => state.wallet); + + useEffect(() => { + dispatch( + getAllValidators({ + baseURL: chainInfo.config.rest, + status: null, + }) + ); + + dispatch( + getDelegations({ + baseURL: chainInfo.config.rest, + address: address, + }) + ); + }, []); + + var createRes = useSelector((state) => state.group.groupProposalRes); + + let navigate = useNavigate(); + + useEffect(() => { + if (createRes?.status === "rejected") { + dispatch( + setError({ + type: "error", + message: createRes?.error, + }) + ); + } else if (createRes?.status === "idle") { + dispatch( + setError({ + type: "success", + message: "Transaction created", + }) + ); + + setTimeout(() => { + navigate(`/groups/${id}/policies/${policyAddress}`); + }, 200); + } + }, [createRes?.status]); + + useEffect(() => { + return () => { + dispatch(resetCreateGroupProposalRes()) + } + }, []) + + + const onSubmit = data => { + let msg = { + }; + + switch (data.txType) { + case TYPE_SEND: + msg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: address, + toAddress: data.toAddress, + amount: [getAmountInAtomics(data.amount, currency)] + }, + ...msg + } + break; + + case TYPE_DELEGATE: + msg = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: { + delegatorAddress: address, + validatorAddress: data?.validator?.value, + amount: getAmountInAtomics(data.amount, currency) + }, + ...msg + } + break; + } + + dispatch(txCreateGroupProposal( + { + metadata: data?.metadata, + admin: wallet?.address, + proposers: [wallet?.address], + messages: [msg], + groupPolicyAddress: address, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config.currencies[0].coinMinimalDenom, + feeAmount: data?.fees, + memo: data?.memo, + gas: data?.gas + } + )) + }; + + + return ( + + + + + + Select Transaction + + + + +
+ { + txType && ( + ( + + )} + /> + ) || null + } + {txType === TYPE_SEND ? ( + + ) : null} + + {txType === TYPE_DELEGATE ? ( + + ) : null} + + {txType === TYPE_REDELEGATE ? ( + + ) : null} + + {txType === TYPE_UNDELEGATE ? ( + + ) : null} + + { + txType && || null + } + + { + txType && + + + + || null + } + +
+
+ ) +} + +export default AddManualTx \ No newline at end of file diff --git a/src/pages/group/AdminGroupList.jsx b/src/pages/group/AdminGroupList.jsx new file mode 100644 index 000000000..a8989b1c6 --- /dev/null +++ b/src/pages/group/AdminGroupList.jsx @@ -0,0 +1,53 @@ +import React, { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import GroupList from '../../components/group/GroupList'; +import { getGroupsByAdmin } from '../../features/group/groupSlice'; + +function AdminGroupList() { + const [adminTotal, setAdminTotal] = useState(0); + const limit = 9; + const dispatch = useDispatch(); + + const address = useSelector((state) => state.wallet.address); + const chainInfo = useSelector((state) => state.wallet.chainInfo); + const groups = useSelector((state) => state.group.groups); + + const fetchGroupsByAdmin = (offset = 0, limit = 9) => { + + dispatch( + getGroupsByAdmin({ + baseURL: chainInfo.config.rest, + admin: address, + pagination: { + offset, + limit + } + }) + ); + } + + useEffect(() => { + fetchGroupsByAdmin(0, limit); + }, [address]); + + useEffect(() => { + if (Number(groups?.admin?.pagination?.total)) + setAdminTotal(Number(groups?.admin?.pagination?.total)) + }, [groups?.admin?.pagination?.total]) + + const handlePagination = (page) => { + fetchGroupsByAdmin(page, limit) + } + + return ( + + ) +} + +export default AdminGroupList \ No newline at end of file diff --git a/src/pages/group/CreateGroup.jsx b/src/pages/group/CreateGroup.jsx index 773b4bd2b..d48d96209 100644 --- a/src/pages/group/CreateGroup.jsx +++ b/src/pages/group/CreateGroup.jsx @@ -1,125 +1,75 @@ import Button from "@mui/material/Button"; -import React, { useState } from "react"; -import DialogAddGroupMember from "../../components/group/DialogAddGroupMember"; -import { Grid, IconButton, Paper, TextField, Typography } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { Paper, TextField, Typography } from "@mui/material"; import { Box } from "@mui/system"; -import { Controller, useForm } from "react-hook-form"; -import { shortenAddress } from "../../utils/util"; -import DeleteOutline from "@mui/icons-material/DeleteOutline"; -import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; -import DialogAttachPolicy from "../../components/group/DialogAttachPolicy"; -import { useDispatch } from "react-redux"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { useDispatch, useSelector } from "react-redux"; +import { resetGroupTx, txCreateGroup } from "../../features/group/groupSlice"; +import CreateGroupForm from "./CreateGroupForm"; +import CreateGroupPolicy from "./CreateGroupPolicy"; +import { useNavigate } from "react-router-dom"; +import PersonAddAltIcon from '@mui/icons-material/PersonAddAlt'; +import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; + export default function CreateGroupPage() { - const [showMemberDialog, setShowMemberDialog] = useState(false); - const [members, setMembers] = useState([]); + const [showAddPolicyForm, setShowAddPolicyForm] = useState(null); - const showGroupMemberDialog = () => { - setShowMemberDialog(true); - }; + const wallet = useSelector((state) => state.wallet); + const { chainInfo, connected, address } = wallet; + const navigate = useNavigate(); + const txCreateGroupRes = useSelector(state => state?.group?.txCreateGroupRes); - const { - handleSubmit, - control, - setValue, - formState: { errors }, - } = useForm({ - defaultValues: { - metadata: "", - }, - }); + useEffect(() => { + return () => { + dispatch(resetGroupTx()); + } + }, []) + + useEffect(() => { + if (txCreateGroupRes?.status === 'idle') { + navigate(`/group`) + } + }, [txCreateGroupRes?.status]) const dispatch = useDispatch(); const onSubmit = (data) => { - console.log(data); - console.log(members); - console.log(policyData); - // dispatch({ - // granter: address, - // grantee: data.grantee, - // spendLimit: amountToMinimalValue(data.spendLimit, chainInfo.config.currencies[0]), - // expiration: data.expiration, - // denom: currency.coinMinimalDenom, - // chainId: chainInfo.config.chainId, - // rpc: chainInfo.config.rpc, - // feeAmount: chainInfo.config.gasPriceStep.average, - // }) - }; + const dataObj = { + admin: address, + granter: address, + grantee: data.grantee, + members: data.members, + groupMetaData: data?.metadata, + expiration: data?.expiration, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom + } - const [memberFields, setMemberFields] = useState({ - address: "", - weight: "", - metadata: "", - }); - const removeMember = (address) => { - const newMembers = members.filter(function (member) { - return member.address != address; - }); - setMembers(newMembers); - }; + if (data.policyMetadata) { + dataObj['policyData'] = data.policyMetadata + } - const editMember = (address, weight, metadata) => { - setMemberFields({ - address: address, - weight: weight, - metadata: metadata, - }); - setShowMemberDialog(true); + dispatch(txCreateGroup(dataObj)) }; - const [policyDialogOpen, setpolicyDialogOpen] = useState(false); - const [policyData, setPolicyData] = useState({}); - const onAttachPolicy = (data) => { - setpolicyDialogOpen(false); - setPolicyData(data); - }; - return ( - <> - {showMemberDialog ? ( - { - for (let i = 0; i < members.length; i++) { - if (members[i].address === address) { - members[i] = { - address: address, - metadata: metadata, - weight: weight, - }; - setShowMemberDialog(false); - return; - } - } + const { register, control, + handleSubmit, + watch, + setValue, + formState: { errors }, + reset, trigger, setError } = useForm({}); - setMembers([ - ...members, - { - address: address, - metadata: metadata, - weight: weight, - }, - ]); + const { fields, append, remove } = useFieldArray({ + control, + name: "members" + }); - setShowMemberDialog(false); - }} - onClose={() => setShowMemberDialog(false)} - /> - ) : ( - <> - )} - {policyDialogOpen ? ( - setpolicyDialogOpen(false)} - onAttachPolicy={onAttachPolicy} - members={members.reduce((a, o) => Number(a)+Number(o.weight), 0)} - /> - ) : ( - <> - )} + return ( + Create Group @@ -130,6 +80,8 @@ export default function CreateGroupPage() { sx={{ "& .MuiTextField-root": { mt: 1.5, mb: 1.5 }, p: 2, + width: '70%', + margin: '0 auto' }} >
@@ -143,173 +95,90 @@ export default function CreateGroupPage() { {...field} required label="Group Metadata" + multiline + name="Group Metadata" fullWidth /> )} /> -
- {members.length > 0 ? ( - - Members - - ) : ( - "" - )} - {members.map((member, index) => ( - - ))} -
- {policyData.metadata?.length > 0 ? ( -
- - DecisionPolicy - - - - - Metadata - - - {policyData.metadata} - - - - Voting Period - - {policyData.votingPeriod} - - - - - Policy As Admin - - - {policyData.policyAsAdmin == true ? "true" : "false"} - - - - - Min Execution Period - - - {policyData.minExecutionPeriod} - - - {policyData.decisionPolicy === "threshold" ? ( - - - Threshold - - - {policyData.threshold} - - - ) : ( - - - Percentage - - - {policyData.percentage}% - - - )} - -
- ) : ( - <> - )} + - - {members.length > 0 ? ( - - ) : ( - <> - )} -   - + Add Group Member + || null + } + + + + { + fields?.length && ( + !showAddPolicyForm && || null) || null + } + + { + showAddPolicyForm && + { + setValue('policyMetadata', null) + setShowAddPolicyForm(false) + }} + reset={reset} + register={register} + errors={errors} + fields={fields} + watch={watch} + control={control} /> + } +
- - ); -} - -function MemberItem(props) { - const { address, weight, metadata } = props.member; - const { onRemove, onEdit } = props; - - return ( - - - - -
- onRemove(address)} - > - - - onEdit(address, weight, metadata)} - > - - -
); } diff --git a/src/pages/group/CreateGroupForm.jsx b/src/pages/group/CreateGroupForm.jsx new file mode 100644 index 000000000..ca68a0e69 --- /dev/null +++ b/src/pages/group/CreateGroupForm.jsx @@ -0,0 +1,94 @@ +import { + Box, TextField, IconButton,Grid, +} from '@mui/material'; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import { Controller } from "react-hook-form"; +import React from 'react'; +import AddIcon from '@mui/icons-material/Add'; + +export function CreateGroupForm({ + fields, + control, + append, + remove +}) { + return ( + + + { + fields.map((item, index) => { + return + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + remove(index)} color='error'> + + + { + append({ address: '', metadata: '', weight: 0 }) + }} color='primary'> + + + + + }) + } + + + ) +} + +export default CreateGroupForm \ No newline at end of file diff --git a/src/pages/group/CreateGroupPolicy.js b/src/pages/group/CreateGroupPolicy.js new file mode 100644 index 000000000..55888eeda --- /dev/null +++ b/src/pages/group/CreateGroupPolicy.js @@ -0,0 +1,214 @@ +import { + Box, Button, TextField, Select, MenuItem, FormControlLabel, Switch, Typography, Grid, FormControl, InputLabel, InputAdornment, Paper, +} from '@mui/material'; +import React, { useState } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; +import { Controller } from "react-hook-form"; + +function CreateGroupPolicy({ + control, + register, + watch, + fields, + errors, + reset, + handleCancelPolicy }) { + + return ( + + + + Add Decision Policy + + + + + + + ( + + )} + /> + + + ( + + + Decision Policy + + + + )} + /> + + + { + watch('policyMetadata.decisionPolicy') === 'percentage' ? + ( + + + + )} + /> + : + ( + + + + )} /> + } + + + ( + + Sec, + }} + error={errors?.policyMetadata?.votingPeriod} + helperText={errors?.policyMetadata?.votingPeriod?.message} + /> + + )} /> + + + + ( + + Sec, + }} + error={errors?.policyMetadata?.minExecPeriod} + helperText={errors?.policyMetadata?.minExecPeriod?.message} + /> + + )} /> + + + + + + } + label="Group policy as admin" + labelPlacement="start" + /> + + if set to true, the group policy account address will be used as + group and policy admin + + + + + + + + ) +} + +export default CreateGroupPolicy \ No newline at end of file diff --git a/src/pages/group/CreateProposal.jsx b/src/pages/group/CreateProposal.jsx new file mode 100644 index 000000000..67589dce1 --- /dev/null +++ b/src/pages/group/CreateProposal.jsx @@ -0,0 +1,68 @@ +import { Button, Paper, TextField, Typography } from '@mui/material' +import { Box } from '@mui/system'; +import React from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { useForm, Controller, FormProvider } from "react-hook-form"; + +import PagePolicyTx from './PagePolicyTx'; +import { txCreateGroupProposal } from '../../features/group/groupSlice'; +import TxTypeComponent from '../../components/group/TxTypeComponent'; +import AddManualTx from './AddManualTx'; +import AddFileTx from './AddFileTx'; + +function CreateProposal() { + const [type, setType] = React.useState(null); + const { policyAddress } = useParams(); + const dispatch = useDispatch(); + + const wallet = useSelector(state => state.wallet); + const chainInfo = wallet?.chainInfo; + + const onSubmit = (data) => { + data.groupPolicyAddress = policyAddress; + data.messages = data.msgs; + data.proposers = [wallet?.address]; + data.admin = wallet?.address; + data.chainId = wallet?.chainInfo?.config?.chainId + data.rpc = wallet?.chainInfo?.config?.rpc; + data.denom = wallet?.chainInfo?.config?.currencies?.[0]?.coinMinimalDenom || '' + data.feeAmount = wallet?.chainInfo?.config?.gasPriceStep?.average || 0; + console.log('fee amount', data) + // dispatch(txCreateGroupProposal(data)); + } + + return ( + <> + + Create Policy Proposal + + + + Proposer +
+ {wallet?.address}
+
+ + + { + setType(type) + }} /> + + + + {type === 'single' && setType(null)} + /> || null} + {type === 'multiple' && || null} + +
+ + + ) +} + +export default CreateProposal \ No newline at end of file diff --git a/src/pages/group/Group.jsx b/src/pages/group/Group.jsx new file mode 100644 index 000000000..e90034afd --- /dev/null +++ b/src/pages/group/Group.jsx @@ -0,0 +1,421 @@ +import React, { useEffect, useState } from 'react'; +import { experimentalStyled as styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Grid from '@mui/material/Grid'; +import { Alert, Button, Card, CircularProgress, FormControl, IconButton, Table, TableCell, TableRow, TextField, Typography } from '@mui/material'; +import { useNavigate, useParams } from 'react-router-dom'; +import RowItem from '../../components/group/RowItem'; +import { useDispatch, useSelector } from 'react-redux'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import { getGroupById, getGroupMembers, getGroupMembersById, getGroupPoliciesById, resetUpdateGroupMember, txAddGroupPolicy, txLeaveGroupMember, txUpdateGroupAdmin, txUpdateGroupMember, txUpdateGroupMetadata, txUpdateGroupPolicy } from '../../features/group/groupSlice'; + +import MembersTable from '../../components/group/MembersTable'; +import PolicyCard from '../../components/group/PolicyCard'; +import CreateGroupForm from './CreateGroupForm'; +import UpdateGroupMemberForm from '../../components/group/UpdateGroupMemberForm'; +import CreateGroupPolicy from './CreateGroupPolicy'; +import EditIcon from '@mui/icons-material/Edit'; +import { UpdateGroupAdmin } from '../../txns/group/group'; +import PolicyForm from '../../components/group/PolicyForm'; +import { groupStyles } from './group-css'; +import CancelIcon from '@mui/icons-material/Cancel'; +import CheckIcon from '@mui/icons-material/Check'; +import GroupTab, { TabPanel } from '../../components/group/GroupTab'; +import GroupInfo from './GroupInfo'; +import ActiveProposals from './ActiveProposals'; +import { useForm } from 'react-hook-form'; + + +const Item = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + boxShadow: 'none', + border: '1px solid #908d8d', + padding: theme.spacing(2), + textAlign: 'center', + color: theme.palette.text.secondary, +})); + +const GroupPolicies = ({ id, wallet }) => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [showForm, setShowForm] = useState(false); + const [limit, setLimit] = useState(5); + const [total, setTotal] = useState(0); + const [pageNumber, setPageNumber] = useState(0); + const groupDetails = useSelector(state => state.group.groupInfo); + const { data: groupInformation, status: groupInfoStatus } = groupDetails; + + const addPolicyRes = useSelector(state => state.group.addGroupPolicyRes); + + const groupMembers = useSelector(state => state.group.groupMembers); + + useEffect(() => { + if (addPolicyRes?.status === 'idle') { + getPolicies(limit, '') + setShowForm(false); + } + }, [addPolicyRes?.status]) + + const getPolicies = (limit, key = '') => { + dispatch(getGroupPoliciesById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { limit: limit, key }, + })) + } + useEffect(() => { + getPolicies(limit, '') + }, []) + + const getGroupmembers = () => { + dispatch(getGroupMembersById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { limit: limit, key: '' }, + })) + } + + useEffect(() => { + getGroupmembers(); + }, []) + + const handlePagination = (number, limit, key) => { + setLimit(limit); + setPageNumber(number); + getPolicies(limit, key) + } + + let group_policies = []; + const groupInfo = useSelector(state => state.group.groupPolicies); + const { data, status } = groupInfo; + + if (data) { + group_policies = data?.group_policies; + } + + useEffect(() => { + if (Number(data?.pagination?.total)) + setTotal(Number(data?.pagination?.total || 0)) + }, [data]) + + const handlePolicy = (policyObj) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txAddGroupPolicy({ + admin: groupInformation?.info?.admin, + groupId: id, + policyMetadata: policyObj?.policyMetadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + const { register, control, + handleSubmit, + watch, + formState: { errors }, + reset, trigger, setError } = useForm({}); + + return ( + + + + + { + showForm && + + { + groupMembers?.status === 'pending' ? + + Wait fetching group members information ... + : null + } + + { + groupMembers?.status === 'idle' && + Number(groupMembers?.data?.pagination?.total) > 0 ? +
+ setShowForm(false)} + /> + + + + + : + + No members found on this group. + + } + + +
+ } + + + + { + status === 'pending' ? + : null + } + { + (status !== 'pending' && + !showForm && + !group_policies?.length) && ( + + + No policies found. + + + ) + } + + {status !== 'pending' && group_policies.map((p, index) => ( + + + + ))} + +
+ ) +} + +const GroupMembers = ({ id, wallet }) => { + const dispatch = useDispatch(); + const [limit, setLimit] = useState(5); + const [total, setTotal] = useState(0); + const [pageNumber, setPageNumber] = useState(0); + + const createGroupRes = useSelector(state => state.group?.updateGroupRes) + + const getGroupmembers = () => { + dispatch(getGroupMembersById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { limit: limit, key: '' }, + })) + } + + useEffect(() => { + dispatch(resetUpdateGroupMember()) + getGroupmembers(); + }, [createGroupRes?.status]) + + useEffect(() => { + getGroupmembers(); + }, []) + + const handleMembersPagination = (number, limit, key) => { + setLimit(limit); + setPageNumber(number); + dispatch(getGroupMembersById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { limit: limit, key: key }, + })) + } + + const groupInfo = useSelector(state => state.group.groupMembers); + const { data, status } = groupInfo; + + useEffect(() => { + if (Number(data?.pagination?.total)) + setTotal(Number(data?.pagination?.total || 0)) + }, [data]) + + const handleDeleteMember = (deleteMemberObj) => { + const chainInfo = wallet?.chainInfo; + + const dataObj = { + admin: wallet?.address, + groupId: id, + members: [deleteMemberObj], + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + } + + dispatch(txUpdateGroupMember(dataObj)) + } + + return ( + + { + status === 'pending' ? + : null + } + { + status !== 'pending' ? + + + : null} + + ) +} + + + +const UpdateGroupMember = ({ id, wallet }) => { + const [showForm, setShowForm] = useState(false); + var [members2, setMembers] = useState([]); + const dispatch = useDispatch(); + const updateRes = useSelector(state => state.group.updateGroupRes) + + useEffect(() => { + if (updateRes?.status === 'idle') { + setShowForm(false); + } + }, [updateRes]) + + useEffect(() => { + return () => { + dispatch(resetUpdateGroupMember()) + } + }, []) + + const groupMembers = useSelector(state => state.group.groupMembers) + const members1 = [{ address: '', weight: '', metadata: '' }]; + const { data: members, status: memberStatus } = groupMembers; + console.log({ members }) + + const getMembers = () => { + let m = members && members?.members?.map(m => { + let { added_at, ...newObj } = m?.member; + return newObj + }) + if (m?.length) + setMembers([...m]) + else + setMembers([...members1]); + } + + // useEffect(() => { + // getMembers() + // }, []) + + const getGroupmembers = () => { + dispatch(getGroupMembersById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { limit: 200, key: '' }, + })) + } + + useEffect(() => { + getMembers(); + }, [memberStatus]) + + useEffect(() => { + getGroupmembers(); + }, []) + + const handleUpdate = (allMembers) => { + const chainInfo = wallet?.chainInfo; + + console.log({ allMembers }) + const dataObj = { + admin: wallet?.address, + groupId: id, + members: allMembers?.members, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + } + + dispatch(txUpdateGroupMember(dataObj)) + } + + return ( + + + + + + { + showForm ? + + + { + memberStatus?.status === 'pending' ? : + { + setShowForm(false); + }} + handleUpdate={handleUpdate} + members={members2} + /> + } + + + : null + } + + + + ) +} + +function Group() { + const params = useParams(); + const [tabIndex, setTabIndex] = useState(0); + + const wallet = useSelector(state => state.wallet) + const arr = ['Members', 'Decision Policies', 'Active Proposals'] + + return ( + + + + + setTabIndex(i)} /> + + + + + + + + + + + + + ) +} + +export default Group \ No newline at end of file diff --git a/src/pages/group/GroupInfo.jsx b/src/pages/group/GroupInfo.jsx new file mode 100644 index 000000000..7736d03de --- /dev/null +++ b/src/pages/group/GroupInfo.jsx @@ -0,0 +1,377 @@ +import { + Box, Button, CircularProgress, Grid, Paper, + TextField, + Typography, IconButton, Tooltip +} from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import GroupsIcon from '@mui/icons-material/Groups'; +import CancelIcon from '@mui/icons-material/Cancel'; +import CheckIcon from '@mui/icons-material/Check'; +import EditIcon from '@mui/icons-material/Edit'; + +import { getGroupById, txLeaveGroupMember, txUpdateGroupAdmin, txUpdateGroupMetadata } from "../../features/group/groupSlice"; + +const GroupInfo = ({ id, wallet }) => { + const dispatch = useDispatch(); + const [showAdminInput, setShowAdminInput] = useState(false); + const [admin, setAdmin] = useState(''); + const [metadata, setMetadata] = useState(''); + const [showMetadataInput, setShowMetadataInput] = useState(false); + const [groupInformation, setGroupInformation] = useState({}); + + const groupMembers = useSelector(state => state.group.groupMembers) + const { data: members, status: memberStatus } = groupMembers; + + const groupInfo = useSelector(state => state.group.groupInfo); + const { data, status } = groupInfo; + + const leaveGroupRes = useSelector(state => state.group.leaveGroupRes); + const updateAdminRes = useSelector(state => state.group.updateGroupAdminRes); + const updateMetadataRes = useSelector(state => state.group.updateGroupMetadataRes); + + + const isExistInGroup = () => { + const existMember = members?.members?.filter(m => m?.member?.address === wallet?.address) + + if (existMember && existMember?.length) return true + + if (groupInformation?.admin === wallet?.address) return true + + return false; + } + + const getGroup = () => { + dispatch(getGroupById({ + baseURL: wallet?.chainInfo?.config?.rest, id: id + })) + } + + useEffect(() => { + getGroup(); + }, []) + + useEffect(() => { + if (groupInfo?.status === 'idle') { + setGroupInformation(groupInfo?.data?.info) + } + + }, [groupInfo?.status]) + + useEffect(() => { + if (updateAdminRes?.status === 'idle') { + setShowAdminInput(false); + getGroup(); + } + + }, [updateAdminRes?.status]) + + useEffect(() => { + if (updateMetadataRes?.status === 'idle') { + setShowMetadataInput(false); + getGroup(); + } + + }, [updateMetadataRes?.status]) + + + const handleLeaveGroup = () => { + const chainInfo = wallet?.chainInfo; + dispatch(txLeaveGroupMember({ + admin: wallet?.address, + groupId: id, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })); + } + + const UpdateAdmin = () => { + const chainInfo = wallet?.chainInfo; + dispatch(txUpdateGroupAdmin({ + signer: wallet?.address, + admin: data?.info?.admin, + groupId: id, + newAdmin: admin, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })); + } + + const UpdateMetadata = () => { + const chainInfo = wallet?.chainInfo; + dispatch(txUpdateGroupMetadata({ + signer: wallet?.address, + admin: data?.info?.admin, + groupId: id, + metadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })); + } + + return ( + + + + + + + # {data?.info?.id} + + { + memberStatus?.status === 'pending' ? + : null + } + { + memberStatus?.status !== 'pending' && isExistInGroup() ? + : null + } + + + + { + status === 'pending' ? + : null + } + { + status !== 'pending' ? + + + + Admin + + + { + showAdminInput ? + { + setAdmin(e.target.value) + }} + /> + : + <> + + {data?.info?.admin || '-'} + + + Note: Only admin can be update admin address. + + + } + + + + { + showAdminInput ? + updateAdminRes?.status === 'pending' ? + : + + + + UpdateAdmin() + } + color="primary"> + + + + + setShowAdminInput(false) + } + color="error"> + + + + + : + isExistInGroup() ? + + { + setAdmin(data?.info?.admin) + setShowAdminInput(!showAdminInput) + } + } /> + + : null + } + + + +
+ + + Metdata + + + { + showMetadataInput ? + { + setMetadata(e.target.value) + }} + /> + : + <> + + {data?.info?.metadata || '-'} + + + Note: Only admin can be update metadata. + + + } + + + + { + showMetadataInput ? + updateMetadataRes?.status === 'pending' ? + : + + + UpdateMetadata() + } + color="primary"> + + + + + + setShowMetadataInput(false) + } + color="error"> + + + + + : + isExistInGroup() ? + + { + setMetadata(data?.info?.metadata) + setShowMetadataInput(!showMetadataInput) + } + } /> + + : null + + + } + + + +
+ + + Version + + + {data?.info?.version || '-'} + + + + + Weight + + + {data?.info?.total_weight || '-'} + + + +
: null + } + +
+
+
+
+ ) +} + +export default GroupInfo \ No newline at end of file diff --git a/src/pages/group/GroupMembersInfo.jsx b/src/pages/group/GroupMembersInfo.jsx new file mode 100644 index 000000000..d678aa699 --- /dev/null +++ b/src/pages/group/GroupMembersInfo.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +function GroupMembersInfo() { + return ( +
GroupMembersInfo
+ ) +} + +export default GroupMembersInfo \ No newline at end of file diff --git a/src/pages/group/MemberGroupList.jsx b/src/pages/group/MemberGroupList.jsx new file mode 100644 index 000000000..da81ba1e0 --- /dev/null +++ b/src/pages/group/MemberGroupList.jsx @@ -0,0 +1,52 @@ +import React, {useState, useEffect} from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import GroupList from '../../components/group/GroupList'; +import { getGroupsByMember } from '../../features/group/groupSlice'; + +function MemberGroupList() { + const [memberTotal, setMemberTotal] = useState(0); + const limit = 9; + const dispatch = useDispatch(); + + const address = useSelector((state) => state.wallet.address); + const chainInfo = useSelector((state) => state.wallet.chainInfo); + const groups = useSelector((state) => state.group.groups); + + const fetchGroupsByMember = (offset = 0, limit = 9) => { + dispatch( + getGroupsByMember({ + baseURL: chainInfo.config.rest, + address: address, + pagination: { + offset, + limit + } + }) + ); + } + + const handlePagination = (page) => { + fetchGroupsByMember(page, limit) + } + + useEffect(() => { + fetchGroupsByMember(0, limit) + }, [address]); + + useEffect(() => { + if (Number(groups?.member?.pagination?.total)) + setMemberTotal(Number(groups?.member?.pagination?.total)) + }, [groups?.member?.pagination?.total]) + + return ( + + ) +} + +export default MemberGroupList \ No newline at end of file diff --git a/src/pages/group/PagePolicyTx.jsx b/src/pages/group/PagePolicyTx.jsx new file mode 100644 index 000000000..49f452108 --- /dev/null +++ b/src/pages/group/PagePolicyTx.jsx @@ -0,0 +1,957 @@ +import react, { useState, useEffect } from "react"; +import { Button, Grid, IconButton, Paper, Typography } from "@mui/material"; +import { useNavigate, useParams } from "react-router-dom"; +import { Box, FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import Send from "../../components/group/bulk/Send"; +import UnDelegateForm from "../../components/group/bulk/UnDelegateForm"; +import RedelegateForm from "../../components/group/bulk/RedelegateForm"; +import { shortenAddress } from "../../utils/util"; +import { Divider } from "@mui/material"; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import { + getAllValidators, + getDelegations, +} from "../../features/staking/stakeSlice"; +import Delegate from "../../components/group/bulk/Delegate"; +import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; +import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; +import { + DELEGATE_TYPE_URL, + parseDelegateMsgsFromContent, + parseReDelegateMsgsFromContent, + parseSendMsgsFromContent, + parseUnDelegateMsgsFromContent, + REDELEGATE_TYPE_URL, + SEND_TYPE_URL, + UNDELEGATE_TYPE_URL, +} from "./utils"; +import { parseBalance } from "../../utils/denom"; +import { Pagination } from "@mui/material"; +import { TextField } from "@mui/material"; +import FeeComponent from "./../../components/multisig/FeeComponent"; +// import { +// createTxn, +// resetCreateTxnState, +// } from "../../features/multisig/multisigSlice"; +import { fee } from "../../txns/execute"; +import { resetError, setError } from "../../features/common/commonSlice"; +import TxBasicFields from "../../components/group/TxBasicFields"; +import { useForm, Controller, FormProvider, useFormContext } from "react-hook-form"; + + +// TODO: serve urls from env + +const MULTISIG_SEND_TEMPLATE = "https://resolute.witval.com/_static/send.csv"; +const MULTISIG_DELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/delegate.csv"; +const MULTISIG_UNDELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/undelegate.csv"; +const MULTISIG_REDELEGATE_TEMPLATE = + "https://resolute.witval.com/_static/redelegate.csv"; + +const PER_PAGE = 6; + +const TYPE_SEND = "SEND"; +const TYPE_DELEGATE = "DELEGATE"; +const TYPE_UNDELEGATE = "UNDELEGATE"; +const TYPE_REDELEGATE = "REDELEGATE"; + +const SelectTransactionType = (props) => { + return ( + + + + + ); +}; + +const FileUpload = (props) => { + const [txType, setTxType] = useState(TYPE_SEND); + return ( + + + + Select Transaction + + + + + + + + + ); +}; + +export default function PagePolicyTx({ control, setValue }) { + const { policyAddress: address } = useParams(); + + const [txType, setTxType] = useState(""); + + const wallet = useSelector((state) => state.wallet); + const { chainInfo, connected } = wallet; + + const validators = useSelector((state) => state.staking.validators); + const methods = useForm({ + defaultValues: { + gas: 20000 + } + }); + const onSubmit = data => console.log(data); + + const dispatch = useDispatch(); + useEffect(() => { + if (connected) { + dispatch( + getAllValidators({ + baseURL: chainInfo.config.rest, + status: null, + }) + ); + + dispatch( + getDelegations({ + baseURL: chainInfo.config.rest, + address: address, + }) + ); + } + }, [connected]); + + const handleTypeChange = (event) => { + setTxType(event.target.value); + }; + + const [messages, setMessages] = useState([]); + + const renderMessage = (msg, index, currency, onDelete) => { + switch (msg.typeUrl) { + case SEND_TYPE_URL: { + return RenderSendMessage(msg, index, currency, onDelete); + } + case DELEGATE_TYPE_URL: + return RenderDelegateMessage(msg, index, currency, onDelete); + case UNDELEGATE_TYPE_URL: + return RenderUnDelegateMessage(msg, index, currency, onDelete); + case REDELEGATE_TYPE_URL: + return RenderReDelegateMessage(msg, index, currency, onDelete); + default: + return ""; + } + }; + + const onDeleteMsg = (index) => { + const arr = messages.filter((_, i) => i !== index); + setMessages(arr); + setValue('msgs', arr) + }; + + const onFileContents = (content, type) => { + switch (type) { + case TYPE_SEND: { + const [parsedTxns, error] = parseSendMsgsFromContent(address, content); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + setValue('msgs', parsedTxns) + } + break; + } + case TYPE_DELEGATE: { + const [parsedTxns, error] = parseDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + setValue('msgs', parsedTxns) + } + break; + } + case TYPE_REDELEGATE: { + const [parsedTxns, error] = parseReDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + setValue('msgs', parsedTxns) + } + break; + } + case TYPE_UNDELEGATE: { + const [parsedTxns, error] = parseUnDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: "error", + message: error, + }) + ); + } else { + setMessages(parsedTxns); + setValue('msgs', parsedTxns); + } + break; + } + default: + setMessages([]); + setValue('msgs', []) + } + }; + + const [mode, setMode] = useState(""); + + const [slicedMsgs, setSlicedMsgs] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + // var createRes = useSelector((state) => state.multisig.createTxnRes); + + let navigate = useNavigate(); + // useEffect(() => { + // if (createRes?.status === "rejected") { + // dispatch( + // setError({ + // type: "error", + // message: createRes?.error, + // }) + // ); + // } else if (createRes?.status === "idle") { + // dispatch( + // setError({ + // type: "success", + // message: "Transaction created", + // }) + // ); + + // setTimeout(() => { + // navigate(`/multisig/${address}/txs`); + // }, 200); + // } + // }, [createRes]); + + useEffect(() => { + return () => { + dispatch(resetError()); + // dispatch(resetCreateTxnState()); + }; + }, []); + + useEffect(() => { + if (messages.length < PER_PAGE) { + setSlicedMsgs(messages); + } else { + setCurrentPage(1); + setSlicedMsgs(messages?.slice(0, 1 * PER_PAGE)); + } + }, [messages]); + + // const { handleSubmit, control, setValue } = useForm({ + // defaultValues: { + // msgs: [], + // gas: 200000, + // memo: "", + // fees: + // chainInfo?.config?.gasPriceStep?.average * + // 10 ** chainInfo?.config?.currencies[0].coinDecimals, + // }, + // }); + + // const onSubmit = (data) => { + // const feeObj = fee( + // chainInfo?.config.currencies[0].coinMinimalDenom, + // data.fees, + // data.gas + // ); + // dispatch( + // createTxn({ + // address: address, + // chainId: chainInfo?.config?.chainId, + // msgs: messages, + // fee: feeObj, + // memo: data.memo, + // gas: data.gas, + // }) + // ); + // }; + + return ( + <> + + {!connected ? ( + + Wallet is not connected + + ) : ( + + + + {mode === "" ? ( + setMode(mode)} /> + ) : mode === "manual" ? ( + <> + + + Select Transaction + + + + + ) : ( + setMode('') + } + onFileContents={(content, type) => + onFileContents(content, type) + } + /> + )} + + { + mode === 'manual' ? + +
+ {txType === "Send" ? ( + { + console.log('value---', payload) + setMessages([...messages, payload]); + setValue('msgs', [...messages, payload]) + }} + /> + ) : null} + + {txType === "Delegate" ? ( + { + setMessages([...messages, payload]); + setValue('msgs', [...messages, payload]) + }} + /> + ) : null} + + {txType === "Redelegate" ? ( + { + setMessages([...messages, payload]); + setValue('msgs', [...messages, payload]); + }} + /> + ) : null} + + {txType === "Undelegate" ? ( + { + setMessages([...messages, payload]); + setValue('msgs', [...messages, payload]) + }} + /> + ) : null} + + + + + +
: null + } + {/* */} +
+
+ + + + + Messages + + + {messages.length === 0 ? ( + + No Messages + + ) : null} + {slicedMsgs.map((msg, index) => { + return ( + + {renderMessage( + msg, + index + PER_PAGE * (currentPage - 1), + chainInfo.config.currencies[0], + onDeleteMsg + )} + + + ); + })} + + {messages.length > 0 ? ( + { + setCurrentPage(v); + setSlicedMsgs( + messages?.slice((v - 1) * PER_PAGE, v * PER_PAGE) + ); + }} + /> + ) : null} + + {messages.length > 0 ? ( + + // + // + // ( + // + // )} + // /> + // + // + // ( + // + // )} + // /> + // + // + // { + // setValue( + // "fees", + // Number(v) * + // 10 ** + // chainInfo?.config?.currencies[0].coinDecimals + // ); + // }} + // chainInfo={chainInfo} + // /> + // + + // {/* + // + // */} + // + ) : null} + + + +
+ )} + + ); +} + +export const RenderSendMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Send  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + to  + + + {shortenAddress(message.value.toAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Delegate  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + to  + + + {shortenAddress(message.value.validatorAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderUnDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Undelegate  + + + {parseBalance( + [message.value.amount], + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + from  + + + {shortenAddress(message.value?.validatorAddress || "", 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; + +export const RenderReDelegateMessage = (message, index, currency, onDelete) => { + return ( + + + + #{index + 1}   + + + Redelegate  + + + {parseBalance( + message.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} + {currency.coinDenom}  + + + from  + + + {shortenAddress(message.value.validatorSrcAddress, 21)}  + + + to  + + + {shortenAddress(message.value.validatorDstAddress, 21)} + + + {onDelete ? ( + onDelete(index)} + > + + + ) : null} + + ); +}; diff --git a/src/pages/group/Policy.jsx b/src/pages/group/Policy.jsx new file mode 100644 index 000000000..79685bd70 --- /dev/null +++ b/src/pages/group/Policy.jsx @@ -0,0 +1,694 @@ +import { + Box, Button, FormControl, + Paper, + Grid, + TextField, Typography, InputAdornment, IconButton, FormLabel, MenuItem, Select, InputLabel, Card, CircularProgress, Alert, +} from '@mui/material' +import React, { useEffect, useState } from 'react' +import { experimentalStyled as styled } from '@mui/material/styles'; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import ProposalSendForm from './ProposalSendForm'; +import ProposalDelegateForm from './ProposalDelegateForm'; +import ProposalRedelegateForm from './ProposalRedelegateForm'; +import ProposalUndelegateForm from './ProposalUndelegateForm'; +import RowItem from '../../components/group/RowItem'; +import { getAmountObj, getLocalStorage, proposalStatus, shortenAddress } from '../../utils/util'; +import { useDispatch, useSelector } from 'react-redux'; +import { getGroupPolicyProposals, txCreateGroupProposal, txGroupProposalExecute, txGroupProposalVote, txUpdateGroupPolicy, txUpdateGroupPolicyAdmin, txUpdateGroupPolicyMetdata } from '../../features/group/groupSlice'; +import { useNavigate, useParams } from 'react-router-dom'; +import DialogVote from '../../components/group/DialogVote'; +import EastIcon from '@mui/icons-material/East'; +import PolicyForm from '../../components/group/PolicyForm'; +import EditIcon from '@mui/icons-material/Edit'; +import CancelIcon from '@mui/icons-material/Cancel'; +import PolicyInfo from './PolicyInfo'; +import PolicyProposalsList from './PolicyProposalsList'; +import AddIcon from '@mui/icons-material/Add'; + +const DELEGATE_MSG = `/cosmos.staking.v1beta1.MsgDelegate`; +const SEND_MSG = `/cosmos.bank.v1beta1.MsgSend`; + +const voteStatus = { + STATUS_CLOSED: 'Closed', +} + +const Item = styled(Paper)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + ...theme.typography.body2, + padding: theme.spacing(2), + boxShadow: 'none', + textAlign: 'center', + color: theme.palette.text.secondary, +})); + +const DelegateMsgComponent = ({ msg, index, deleteMsg }) => { + return ( + + Delegate + {`${msg?.value?.amount?.amount} `} + {`${msg?.value?.amount?.denom} `} + To + { + shortenAddress(msg?.value?.validatorAddress, 30) + } + + deleteMsg(index)} + sx={{ float: 'right', color: 'red' }}> + + + + + ) +} + +const SendMsgComponent = ({ msg, index, deleteMsg }) => { + return ( + + Send + {`${msg?.value?.amount?.[0]?.amount} `} + {`${msg?.value?.amount?.[0]?.denom} `} + To + { + shortenAddress(msg?.value?.toAddress, 30) + } + + deleteMsg(index)} + sx={{ float: 'right', color: 'red' }}> + + + + + ) +} + +const CreateProposal = ({ policyInfo }) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const params = useParams(); + const wallet = useSelector(state => state.wallet); + const [showMsgFrom, setShowMsgForm] = useState(true); + const denom = wallet?.chainInfo?.config?.currencies?.[0].coinMinimalDenom || '-' + console.log('wallet', wallet) + const proposerObj = { + name: 'proposer', + placeholder: 'Enter Proposer Address', + value: '', + } + + const [showCreateProposal, setShowCreateProposal] = useState(false); + var [proposers, setProposers] = useState([{ ...proposerObj }]); + const createProposalRes = useSelector(state => state.group.groupProposalRes) + const [obj, setObj] = useState({ + groupPolicyAddress: policyInfo?.address || '', + messages: [] + }); + + const handleAddProposer = () => { + proposers = [...proposers, proposerObj]; + setProposers([...proposers]); + } + + const handleChange = e => { + obj[e.target.name] = e.target.value; + setObj({ ...obj }); + } + + const handleMsgChange = msgObj => { + let message = {}; + if (msgObj?.typeUrl === DELEGATE_MSG) { + let val = JSON.parse(msgObj?.validatorAddress) + message = { + typeUrl: msgObj?.typeUrl, + value: { + delegatorAddress: msgObj?.delegatorAddress, + validatorAddress: val?.value, + amount: getAmountObj(msgObj?.amount, wallet?.chainInfo) + } + } + } + + if (msgObj?.typeUrl === SEND_MSG) { + message = { + typeUrl: msgObj?.typeUrl, + value: { + fromAddress: msgObj?.fromAddress, + toAddress: msgObj?.toAddress, + amount: [getAmountObj(msgObj?.amount, wallet?.chainInfo)] + } + } + } + + obj.messages = [...obj?.messages, message]; + setObj({ ...obj }); + setShowMsgForm(false); + } + + const onRemove = (index) => { + proposers.splice(index, 1); + setProposers([...proposers]) + } + + const handleSubmit = (e) => { + e.preventDefault(); + obj['proposers'] = proposers.map(p => p.value); + obj['admin'] = wallet?.address; + obj['chainId'] = wallet?.chainInfo?.config?.chainId + obj['rpc'] = wallet?.chainInfo?.config?.rpc; + obj['denom'] = wallet?.chainInfo?.config?.currencies?.[0]?.coinMinimalDenom || '' + obj['feeAmount'] = wallet?.chainInfo?.config?.gasPriceStep?.average || 0 + dispatch(txCreateGroupProposal(obj)); + } + + const deleteMsg = i => { + obj.messages.splice(i, 1); + setObj({ ...obj }) + } + + return ( + + + + { + showCreateProposal ? + + + +
+

Create Proposal

+ +
+ + +

+

+ { + proposers.map((p, pIndex) => ( + + { + let proposerObj = proposers[pIndex]; + proposerObj.value = e.target.value; + proposers[pIndex] = proposerObj; + setProposers([...proposers]) + }} + InputProps={{ + endAdornment: ( + + { + proposers.length > 1 ? + onRemove(pIndex)} + > + + : null + } + + ), + }} + /> +
+
+ )) + } + +
+



+ + + + + Add Messages + + + + + Select Transaction Type + + + + +

+ + { + showMsgFrom && ( + + { + obj.txType === 'send' ? : null + } + + { + obj.txType === 'delegate' ? : null + } + + { + obj.txType === 'redelegate' ? : null + } + + { + obj.txType === 'undelegate' ? : null + } + + ) + } + + + { + obj?.messages?.length && + + Messages + + { + obj?.messages?.map((m, i) => ( + m.typeUrl === SEND_MSG && () || + m.typeUrl === DELEGATE_MSG && () + )) + } + + + || null} + +

+ + + + + +
+
+
: null + } +
+ ) +} + +const AllProposals = () => { + const dispatch = useDispatch(); + const params = useParams(); + + const proposals = useSelector(state => state.group?.proposals) + const wallet = useSelector(state => state.wallet) + const [voteOpen, setVoteOpen] = useState(false); + const voteRes = useSelector(state => state.group?.voteRes); + const createProposalRes = useSelector(state => state.group?.groupProposalRes); + const navigate = useNavigate(); + + const getProposals = () => { + dispatch(getGroupPolicyProposals({ + baseURL: wallet?.chainInfo?.config?.rest, + address: params?.policyId + })) + } + + useEffect(() => { + if (createProposalRes?.status === 'idle') + getProposals() + + }, [createProposalRes?.status]) + + useEffect(() => { + getProposals(); + }, []) + + const onVoteDailogClose = () => { + setVoteOpen(false); + } + + const onConfirm = (voteObj) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalVote({ + admin: wallet?.address, + voter: wallet?.address, + option: voteObj?.vote, + proposalId: voteObj?.proposalId, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + console.log('vote objj', voteObj) + } + + const onExecute = (proposalId) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalExecute({ + proposalId: proposalId, + admin: wallet?.address, + executor: wallet?.address, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + } + + return ( + +

+ All Proposals + + { + proposals?.status === 'pending' ? + : null + } + + { + proposals?.status !== 'pending' && + !proposals?.data?.proposals?.length ? + + + No proposals found. + + + : null + } + + {proposals?.status !== 'pending' && proposals?.data?.proposals?.length && + proposals?.data?.proposals?.map((p, index) => ( + + + + + + + # {p?.id} + + + + {proposalStatus[p?.status]?.label || p?.status} + + + + { + p?.status === 'PROPOSAL_STATUS_SUBMITTED' ? + + : null + } + + { + p?.status === 'PROPOSAL_STATUS_ACCEPTED' ? + + : null + } + + + + + + + + + + + + + + Proposers + + + + { + p?.proposers?.map(p1 => ( + + - {shortenAddress(p1, 19)} + + )) + } + + + + + + + + + Messages + + + + { + p?.messages?.map(m => ( + + { + m['@type'] === SEND_MSG ? + + - Send + { + `${m?.amount?.[0]?.amount} ` + } + { + `${m?.amount?.[0]?.denom} ` + } + To + { + `${shortenAddress(m?.to_address, 19)}` + } + : null + } + { + m['@type'] === DELEGATE_MSG ? + + + - Delegate + { + `${m?.amount?.amount} ` + } + { + `${m?.amount?.denom} ` + } + To + { + `${shortenAddress(m?.validator_address, 19)}` + } + : null + } + + )) + } + + + + + { + navigate(`/groups/proposals/${p?.id}`) + }} + sx={{ + height: '50%', + m: '0 auto', + fontSize: 35, + color: '#000' + }} /> + + + + + + + )) || null} + +
+ ) +} + +function Policy() { + const [showMetdataInput, setShowMetadataInput] = useState(false); + const [showAdminInput, setShowAdminInput] = useState(false); + const [metadata, setMetadata] = useState(''); + const [admin, setAdmin] = useState(''); + const [showEditForm, setShowEditForm] = useState(false); + const dispatch = useDispatch(); + + const wallet = useSelector(state => state.wallet); + const updateMetadataRes = useSelector(state => state.group.updateGroupMetadataRes) + + let policyInfo = {}; + try { + policyInfo = getLocalStorage('policy', 'object'); + } catch (error) { + console.log('Errot while getting policy', error?.message) + } + + useEffect(() => { + if (updateMetadataRes?.status === 'idle') + setShowMetadataInput(false); + }, [updateMetadataRes?.status]) + + const handleSubmitPolicy = (policyMetadata) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txUpdateGroupPolicy({ + admin: policyInfo?.admin, + groupPolicyAddress: policyInfo?.address, + policyMetadata: policyMetadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + const handlePolicyMetadata = () => { + const chainInfo = wallet?.chainInfo; + + dispatch(txUpdateGroupPolicyMetdata({ + admin: policyInfo?.admin, + groupPolicyAddress: policyInfo?.address, + metadata: metadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + const handleUpdateAdmin = () => { + const chainInfo = wallet?.chainInfo; + + dispatch(txUpdateGroupPolicyAdmin({ + admin: policyInfo?.admin, + groupPolicyAddress: policyInfo?.address, + newAdmin: admin, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + return ( + + + + + + ) +} + +export default Policy \ No newline at end of file diff --git a/src/pages/group/PolicyInfo.jsx b/src/pages/group/PolicyInfo.jsx new file mode 100644 index 000000000..9aeadaf3f --- /dev/null +++ b/src/pages/group/PolicyInfo.jsx @@ -0,0 +1,179 @@ +import { + Button, Grid, TextField, + Paper, + Box, + Typography, + CircularProgress +} from '@mui/material'; +import React, { useEffect, useState } from 'react' +import CancelIcon from '@mui/icons-material/Cancel'; +import { useDispatch, useSelector } from 'react-redux'; +import { getLocalStorage, shortenAddress } from '../../utils/util'; +import { getGroupPoliciesById, txUpdateGroupPolicy, txUpdateGroupPolicyAdmin, txUpdateGroupPolicyMetdata } from '../../features/group/groupSlice'; +import PolicyForm from '../../components/group/PolicyForm'; +import EditIcon from '@mui/icons-material/Edit'; +import RowItem from '../../components/group/RowItem'; +import { useParams } from 'react-router-dom'; +import AlertMsg from '../../components/group/AlertMsg'; +import PolicyDetails from '../../components/group/PolicyDetails'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import { useForm } from 'react-hook-form'; +import CreateGroupPolicy from './CreateGroupPolicy' + +function PolicyInfo() { + const [policyObj, setPolicyObj] = useState({}); + const [isEditPolicyForm, setEditPolicyForm] = useState(false); + + const dispatch = useDispatch(); + const { id, policyId } = useParams(); + + const wallet = useSelector(state => state.wallet); + const updateMetadataRes = useSelector(state => state.group.updateGroupMetadataRes); + const updatePolicyAdminRes = useSelector(state => state.group.updatePolicyAdminRes); + const updateGroupPolicyRes = useSelector(state => state.group.updateGroupPolicyRes); + + const groupPoliceis = useSelector(state => state?.group?.groupPolicies) + + const getPolicies = () => { + dispatch(getGroupPoliciesById({ + baseURL: wallet?.chainInfo?.config?.rest, + id: id, + pagination: { + key: '', + limit: 100 + } + })) + } + + useEffect(() => { + getPolicies(); + }, []) + + useEffect(() => { + const data = groupPoliceis?.data?.group_policies || []; + if (data?.length) { + const pArr = data.filter(d => d.address === policyId) + if (pArr?.length) { + setPolicyObj(pArr[0]); + } + } + }, [groupPoliceis?.status]) + + useEffect(() => { + if (updateMetadataRes?.status === 'idle') + getPolicies(); + }, [updateMetadataRes?.status]) + + useEffect(() => { + if (updatePolicyAdminRes?.status === 'idle') + getPolicies(); + }, [updatePolicyAdminRes?.status]) + + useEffect(() => { + if (updateGroupPolicyRes?.status === 'idle') { + setEditPolicyForm(false); + getPolicies(); + } + }, [updateGroupPolicyRes?.status]) + + + const handlePolicyMetadata = (newMetadata) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txUpdateGroupPolicyMetdata({ + admin: policyObj?.admin, + groupPolicyAddress: policyObj?.address, + metadata: newMetadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + const handleUpdateAdmin = (newAdmin) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txUpdateGroupPolicyAdmin({ + admin: policyObj?.admin, + groupPolicyAddress: policyObj?.address, + newAdmin: newAdmin, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + const handleSubmitPolicy = (policyMetadata) => { + const chainInfo = wallet?.chainInfo; + + console.log('handle policy metad-----', policyMetadata) + + dispatch(txUpdateGroupPolicy({ + admin: policyObj?.admin, + groupPolicyAddress: policyObj?.address, + policyMetadata: policyMetadata, + denom: chainInfo?.config?.currencies?.[0]?.minimalCoinDenom, + chainId: chainInfo.config.chainId, + rpc: chainInfo.config.rpc, + feeAmount: chainInfo.config.gasPriceStep.average, + })) + } + + return ( + + Policy Information + { + groupPoliceis?.status === 'pending' ? + + + : null + } + { + (groupPoliceis?.status === 'idle' && + !policyObj?.address) && + } + + { + (groupPoliceis?.status === 'idle' && + policyObj?.address) ? + + + + { + isEditPolicyForm ? + setEditPolicyForm(false)} + /> + + : + + } + + + : null + } + + ) +} + +export default PolicyInfo \ No newline at end of file diff --git a/src/pages/group/PolicyProposalsList.jsx b/src/pages/group/PolicyProposalsList.jsx new file mode 100644 index 000000000..f0e8442c3 --- /dev/null +++ b/src/pages/group/PolicyProposalsList.jsx @@ -0,0 +1,113 @@ +import { + Alert, Card, CircularProgress, + Button, + Grid, Paper, Typography +} from '@mui/material'; +import { Box } from '@mui/system'; +import React, { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate, useParams } from 'react-router-dom'; +import AlertMsg from '../../components/group/AlertMsg'; +import ProposalCard from '../../components/group/ProposalCard'; +import { getGroupPolicyProposals, txGroupProposalExecute, txGroupProposalVote } from '../../features/group/groupSlice'; +import { proposalStatus } from '../../utils/util'; + +function PolicyProposalsList() { + + const dispatch = useDispatch(); + const params = useParams(); + + const proposals = useSelector(state => state.group?.proposals) + console.log('p----------', proposals) + const wallet = useSelector(state => state.wallet) + const [voteOpen, setVoteOpen] = useState(false); + const voteRes = useSelector(state => state.group?.voteRes); + const createProposalRes = useSelector(state => state.group?.groupProposalRes); + const navigate = useNavigate(); + + const getProposals = () => { + dispatch(getGroupPolicyProposals({ + baseURL: wallet?.chainInfo?.config?.rest, + address: params?.policyId + })) + } + + useEffect(() => { + if (createProposalRes?.status === 'idle') + getProposals() + + }, [createProposalRes?.status]) + + useEffect(() => { + getProposals(); + }, []) + + const onVoteDailogClose = () => { + setVoteOpen(false); + } + + const onConfirm = (voteObj) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalVote({ + admin: wallet?.address, + voter: wallet?.address, + option: voteObj?.vote, + proposalId: voteObj?.proposalId, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + console.log('vote objj', voteObj) + } + + const onExecute = (proposalId) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalExecute({ + proposalId: proposalId, + admin: wallet?.address, + executor: wallet?.address, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + } + + return ( + + + All Proposals + + + { + proposals?.status === 'pending' ? + : null + } + + { + (proposals?.status === 'idle' && + !proposals?.data?.proposals?.length) ? + : null + } + + + { + proposals?.data?.proposals?.map(p => ( + + + + )) + } + + + ) +} + +export default PolicyProposalsList \ No newline at end of file diff --git a/src/pages/group/Proposal.jsx b/src/pages/group/Proposal.jsx new file mode 100644 index 000000000..5d2002120 --- /dev/null +++ b/src/pages/group/Proposal.jsx @@ -0,0 +1,366 @@ +import { Alert, Button, Card, Chip, CircularProgress, Grid, Paper, Typography } from '@mui/material' +import { Box } from '@mui/system' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import VotesTable from '../../components/group/VotesTable'; +import RowItem from '../../components/group/RowItem'; +import { getGroupProposalById, getVotesProposalById, txGroupProposalExecute, txGroupProposalVote } from '../../features/group/groupSlice'; +import { proposalStatus, shortenAddress } from '../../utils/util'; +import AlertMsg from '../../components/group/AlertMsg'; +import { getLocalTime } from '../../utils/datetime'; +import DailogVote from '../../components/group/DialogVote'; + +const ProposalInfo = ({ id, wallet }) => { + const [voteOpen, setVoteOpen] = useState(false); + + const dispatch = useDispatch(); + const proposalInfo = useSelector(state => state.group.groupProposal); + const voteRes = useSelector(state => state.group?.voteRes); + + const { data: { proposal } = {} } = proposalInfo; + + const getProposal = () => { + dispatch(getGroupProposalById({ + baseURL: wallet?.chainInfo?.config?.rest, + id + })) + } + + useEffect(() => { + getProposal(); + }, []) + + useEffect(() => { + if (voteRes?.status === 'idle') + setVoteOpen(false); + }, [voteRes?.status]) + + const onVoteDailogClose = () => { + setVoteOpen(false); + } + + const onConfirm = (voteObj) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalVote({ + admin: wallet?.address, + voter: wallet?.address, + option: voteObj?.vote, + proposalId: voteObj?.proposalId, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + console.log('vote objj', voteObj) + } + + const onExecute = (proposalId) => { + const chainInfo = wallet?.chainInfo; + + dispatch(txGroupProposalExecute({ + proposalId: proposalId, + admin: wallet?.address, + executor: wallet?.address, + chainId: chainInfo?.config?.chainId, + rpc: chainInfo?.config?.rpc, + denom: chainInfo?.config?.currencies?.[0]?.coinMinimalDenom, + feeAmount: chainInfo?.config?.gasPriceStep?.average, + })) + } + + return ( + + + { + proposalInfo?.status === 'pending' ? + : null + } + { + (proposalInfo?.status === 'idle' && + !proposal) ? + : null + } + + { + proposalInfo?.status === 'idle' ? + + + + + + + + # {proposal?.metadata || '-'} + + + + + + + Submit Time + + + {getLocalTime(proposal?.submit_time)} + + + + + + Voting Ends + + + {getLocalTime(proposal?.voting_period_end)} + + + + + + Group Policy Address + + + {shortenAddress(proposal?.group_policy_address, 21)} + + + + + + Proposers + + + { + proposal?.proposers?.map(p => ( + <> + + {shortenAddress(p, 21)} + +
+ + )) + } +
+
+
+ + { + proposal?.status === 'PROPOSAL_STATUS_SUBMITTED' ? + + : null + } + + { + proposal?.status === 'PROPOSAL_STATUS_ACCEPTED' ? + + : null + } + +
+ + + + Vote Details

+ + + + Yes + + {proposal?.final_tally_result?.yes_count || 0} + + + + + + + No + + {proposal?.final_tally_result?.no_count || 0} + + + + + + + Abstain + + {proposal?.final_tally_result?.abstain_count || 0} + + + + + + + Veto + {proposal?.final_tally_result?.no_with_veto_account || 0} + + + + +
+ + + Messages + + { + proposal?.messages?.map(p => ( + + { + p['@type'] === '/cosmos.bank.v1beta1.MsgSend' ? + + + Send   + {p?.amount?.[0]?.amount}   + {p?.amount?.[0]?.denom}   + to   + {p?.to_address} + : null + } + { + p['@type'] === '/cosmos.staking.v1beta1.MsgDelegate' ? + + + Delegate   + {p?.amount?.amount} &?.[0]nbsp; + {p?.amount?.denom}   + to   + {p?.validator_address} + : null + } + + )) + } + + +
: null + } +
+ ) +} + +function Proposal() { + const dispatch = useDispatch(); + const params = useParams(); + const { id } = params; + const [limit, setLimit] = useState(5); + const [total, setTotal] = useState(0); + const [pageNumber, setPageNumber] = useState(0); + const wallet = useSelector(state => state.wallet); + const voteRes = useSelector(state => state.group?.voteRes); + + const fetchVotes = (baseURL, id, limit, key) => { + dispatch(getVotesProposalById({ + baseURL: baseURL, + id: id, + pagination: { limit: limit, key: key }, + })) + } + + useEffect(() => { + if (voteRes?.status === 'idle') + fetchVotes(wallet?.chainInfo?.config?.rest, id, limit, '') + }, [voteRes?.status]) + + useEffect(() => { + fetchVotes(wallet?.chainInfo?.config?.rest, id, limit, '') + }, []) + + const handleMembersPagination = (number, limit, key) => { + setLimit(limit); + setPageNumber(number); + fetchVotes(wallet?.chainInfo?.config?.rest, id, limit, key) + } + + const groupInfo = useSelector(state => state.group.proposalVotes); + const { data, status } = groupInfo; + + useEffect(() => { + if (Number(data?.pagination?.total)) + setTotal(Number(data?.pagination?.total || 0)) + }, [data]) + + return ( + + + + + + + + + + + + { + status === 'pending' ? + : null + } + + { + status !== 'pending' ? + + + : null} + + + + ) +} + +export default Proposal \ No newline at end of file diff --git a/src/pages/group/ProposalDelegateForm.jsx b/src/pages/group/ProposalDelegateForm.jsx new file mode 100644 index 000000000..157adda6a --- /dev/null +++ b/src/pages/group/ProposalDelegateForm.jsx @@ -0,0 +1,108 @@ +import { Button, FormControl, InputAdornment, InputLabel, MenuItem, Select, TextField } from '@mui/material' +import { Box } from '@mui/system' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { getValidators } from '../../features/staking/stakeSlice'; + +function ProposalDelegateForm({ chainInfo, + handleMsgChange, + type, + fromAddress }) { + var [data, setData] = useState([]); + const dispatch = useDispatch(); + + const denom = chainInfo?.config?.currencies?.[0]?.coinDenom || '' + const [obj, setObj] = useState({ + typeUrl: type, + delegatorAddress: fromAddress + }); + + var validators = useSelector((state) => state.staking.validators); + validators = validators?.active || {}; + + useEffect(() => { + dispatch( + getValidators({ + baseURL: chainInfo.config.rest, + status: null, + }) + ); + }, []) + + useEffect(() => { + data = []; + Object.entries(validators).map(([k, v], index) => { + let obj1 = { + value: k, + label: v?.description?.moniker || k, + }; + + data = [...data, obj1]; + }); + + setData([...data]); + }, [validators]); + + const handleChange = e => { + obj[e.target.name] = e.target.value; + setObj({ ...obj }); + } + + const onSubmit = () => { + handleMsgChange(obj); + }; + + return ( + + + + + + Select Validator + + + + + {denom} + + ), + }} + /> + + + + + ) +} + +export default ProposalDelegateForm \ No newline at end of file diff --git a/src/pages/group/ProposalRedelegateForm.jsx b/src/pages/group/ProposalRedelegateForm.jsx new file mode 100644 index 000000000..78f932150 --- /dev/null +++ b/src/pages/group/ProposalRedelegateForm.jsx @@ -0,0 +1,108 @@ +import { Button, FormControl, InputAdornment, InputLabel, MenuItem, Select, TextField } from '@mui/material' +import { Box } from '@mui/system' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { getValidators } from '../../features/staking/stakeSlice'; + +function ProposalReDelegateForm({ chainInfo, + handleMsgChange, + type, + fromAddress }) { + var [data, setData] = useState([]); + const dispatch = useDispatch(); + + const denom = chainInfo?.config?.currencies?.[0]?.coinDenom || '' + const [obj, setObj] = useState({ + typeUrl: type, + delegatorAddress: fromAddress + }); + + var validators = useSelector((state) => state.staking.validators); + validators = validators?.active || {}; + + useEffect(() => { + dispatch( + getValidators({ + baseURL: chainInfo.config.rest, + status: null, + }) + ); + }, []) + + useEffect(() => { + data = []; + Object.entries(validators).map(([k, v], index) => { + let obj1 = { + value: k, + label: v?.description?.moniker || k, + }; + + data = [...data, obj1]; + }); + + setData([...data]); + }, [validators]); + + const handleChange = e => { + obj[e.target.name] = e.target.value; + setObj({ ...obj }); + } + + const onSubmit = () => { + handleMsgChange(obj); + }; + + return ( + + + + + + Select Validator + + + + + {denom} + + ), + }} + /> + + + + + ) +} + +export default ProposalReDelegateForm \ No newline at end of file diff --git a/src/pages/group/ProposalSendForm.jsx b/src/pages/group/ProposalSendForm.jsx new file mode 100644 index 000000000..105dca534 --- /dev/null +++ b/src/pages/group/ProposalSendForm.jsx @@ -0,0 +1,47 @@ +import { Box, Button, FormControl, TextField } from '@mui/material' +import React, { useState } from 'react'; + +const TextFieldComponent = ({ name, placeholder, value, + disbled, onChange, type }) => ( + + + +) + +function ProposalSendForm({ handleMsgChange, fromAddress, type }) { + const [obj, setObj] = useState({ + typeUrl: type, + fromAddress: fromAddress, + }); + + const handleChange = e => { + obj[e.target.name] = e.target.value; + setObj({ ...obj }); + } + + return ( + + + + + + + ) +} + +export default ProposalSendForm \ No newline at end of file diff --git a/src/pages/group/ProposalUndelegateForm.jsx b/src/pages/group/ProposalUndelegateForm.jsx new file mode 100644 index 000000000..2e2e3557b --- /dev/null +++ b/src/pages/group/ProposalUndelegateForm.jsx @@ -0,0 +1,110 @@ +import { Button, FormControl, InputAdornment, InputLabel, MenuItem, Select, TextField } from '@mui/material' +import { Box } from '@mui/system' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux'; +import { getDelegations, getValidators } from '../../features/staking/stakeSlice'; + +function ProposalUnDelegateForm({ chainInfo, + handleMsgChange, + type, + address }) { + var [data, setData] = useState([]); + const dispatch = useDispatch(); + + const denom = chainInfo?.config?.currencies?.[0]?.coinDenom || '' + const [obj, setObj] = useState({ + typeUrl: type, + delegatorAddress: address + }); + + var validators = useSelector((state) => state.staking.validators); + var delegatorVals = useSelector((state) => state.multisig.delegatorVals); + console.log('dddddddddd', delegatorVals) + validators = validators?.active || {}; + + useEffect(() => { + dispatch( + getDelegations({ + baseURL: chainInfo.config.rest, + address: address, + }) + ); + }, []) + + useEffect(() => { + data = []; + Object.entries(validators).map(([k, v], index) => { + let obj1 = { + value: k, + label: v?.description?.moniker || k, + }; + + data = [...data, obj1]; + }); + + setData([...data]); + }, [validators]); + + const handleChange = e => { + obj[e.target.name] = e.target.value; + setObj({ ...obj }); + } + + const onSubmit = () => { + handleMsgChange(obj); + }; + + return ( + + + + + + Select Validator + + + + + {denom} + + ), + }} + /> + + + + + ) +} + +export default ProposalUnDelegateForm \ No newline at end of file diff --git a/src/pages/group/group-css.js b/src/pages/group/group-css.js new file mode 100644 index 000000000..dd30f220e --- /dev/null +++ b/src/pages/group/group-css.js @@ -0,0 +1,45 @@ +export const groupStyles = { + fr: { + float: 'right' + }, + mb_2: { + mb: 2 + }, + mt_2: { + mt: 2 + }, + fr_mb_2: { + float: 'right', + mb: 2 + }, + fw: { + width: '100%', + mb: 2 + }, + j_right: { + justifyContent: 'right', + }, + d_flex: { + display: 'flex', + }, + get gp_main() { + return { + ...this.j_right, + ...this.d_flex + } + }, + t_transform_btn: { + textTransform: 'none', + padding: '10px', + fontSize: '16px' + }, + t_align: { + textAlign: 'left' + }, + get btn_g_box() { + return { + ...this.t_align, + ...this.mt_2 + } + } +} \ No newline at end of file diff --git a/src/pages/group/utils.ts b/src/pages/group/utils.ts new file mode 100644 index 000000000..31f03c089 --- /dev/null +++ b/src/pages/group/utils.ts @@ -0,0 +1,206 @@ +import { parseCoins } from "@cosmjs/proto-signing"; +import { Msg } from "../../txns/types"; + +export const SEND_TYPE_URL = "/cosmos.bank.v1beta1.MsgSend"; +export const DELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgDelegate"; +export const UNDELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgUndelegate"; +export const REDELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgBeginRedelegate"; + +// parseSendMsgsFromContent returns list of parsed send messages. It returns an error +// if provided content is invalid. +export const parseSendMsgsFromContent = ( + from: string, + content: string +): [Msg[], string] => { + const messages = content.split("\n"); + + if (messages?.length === 0) { + return [[], "no messages or invalid file content"]; + } + + const msgs = []; + for (let i = 1; i < messages.length; i++) { + try { + const tx = parseSendTx(from, messages[i]); + if (tx && Object.keys(tx)?.length) + msgs.push(tx); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, ""]; +}; + +const parseSendTx = (from: string, msg: string): Msg => { + const values = msg.split(","); + console.log('Parsed values-----', values) + if (values?.length !== 2) { + return {} + // throw new Error( + // `invalid message: expected ${2} values got ${values.length}` + // ); + } + + const to = values[0]; + const amount = parseCoins(values[1]); + + if (amount.length === 0) { + throw new Error("amount cannot be empty"); + } + + return { + typeUrl: SEND_TYPE_URL, + value: { + fromAddress: from, + toAddress: to, + amount: amount, + }, + }; +}; + +// parseDelegateMsgsFromContent returns list of parsed delegate messages. It returns an error +// if provided content is invalid. +export const parseDelegateMsgsFromContent = ( + delegator: string, + content: string +): [Msg[], string] => { + const messages = content.split("\n"); + + if (messages?.length === 0) { + return [[], "no messages or invalid file content"]; + } + + const msgs = []; + for (let i = 0; i < messages.length; i++) { + try { + const msg = parseDelegateMsg(delegator, messages[i]); + msgs.push(msg); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, ""]; +}; + +const parseDelegateMsg = (delegator: string, msg: string): Msg => { + const values = msg.split(","); + if (values?.length !== 2) { + throw new Error("invalid message"); + } + + const validator = values[0]; + const amount = parseCoins(values[1]); + + if (amount.length === 0) { + throw new Error("amount cannot be empty"); + } + + return { + typeUrl: DELEGATE_TYPE_URL, + value: { + delegatorAddress: delegator, + validatorAddress: validator, + amount: amount, + }, + }; +}; + +// parseUnDelegateMsgsFromContent returns list of parsed un-delegate messages. It returns an error +// if provided content is invalid. +export const parseUnDelegateMsgsFromContent = ( + delegator: string, + content: string +): [Msg[], string] => { + const messages = content.split("\n"); + + if (messages?.length === 0) { + return [[], "no messages or invalid file content"]; + } + + const msgs = []; + for (let i = 0; i < messages.length; i++) { + try { + const msg = parseUnDelegateMsg(delegator, messages[i]); + msgs.push(msg); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, ""]; +}; + +const parseUnDelegateMsg = (delegator: string, msg: string): Msg => { + const values = msg.split(","); + if (values?.length !== 2) { + throw new Error("invalid message"); + } + + const validator = values[0]; + const amount = parseCoins(values[1]); + + if (amount.length === 0) { + throw new Error("amount cannot be empty"); + } + + return { + typeUrl: UNDELEGATE_TYPE_URL, + value: { + delegatorAddress: delegator, + validatorAddress: validator, + amount: amount, + }, + }; +}; + +// parseReDelegateMsgsFromContent returns list of parsed re-delegate messages. It returns an error +// if provided content is invalid. +export const parseReDelegateMsgsFromContent = ( + delegator: string, + content: string +): [Msg[], string] => { + const messages = content.split("\n"); + + if (messages?.length === 0) { + return [[], "no messages or invalid file content"]; + } + + const msgs = []; + for (let i = 0; i < messages.length; i++) { + try { + const msg = parseReDelegateMsg(delegator, messages[i]); + msgs.push(msg); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, ""]; +}; + +const parseReDelegateMsg = (delegator: string, msg: string): Msg => { + const values = msg.split(","); + if (values?.length !== 3) { + throw new Error("invalid message"); + } + + const src = values[0]; + const dest = values[1]; + const amount = parseCoins(values[2]); + + if (amount.length === 0) { + throw new Error("amount cannot be empty"); + } + + return { + typeUrl: REDELEGATE_TYPE_URL, + value: { + validatorDstAddress: dest, + validatorSrcAddress: src, + delegatorAddress: delegator, + amount: amount, + }, + }; +}; diff --git a/src/txns/execute.js b/src/txns/execute.js index 73f1dc764..fb23603df 100644 --- a/src/txns/execute.js +++ b/src/txns/execute.js @@ -6,7 +6,7 @@ import { import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { Registry } from "@cosmjs/proto-signing"; import { MsgClaim } from "./passage/msg_claim"; -import { MsgCreateGroup, MsgCreateGroupWithPolicy } from "./group/v1/tx"; +import { MsgCreateGroup, MsgCreateGroupPolicy, MsgCreateGroupWithPolicy, MsgExec, MsgLeaveGroup, MsgSubmitProposal, MsgUpdateGroupAdmin, MsgUpdateGroupMembers, MsgUpdateGroupMetadata, MsgUpdateGroupPolicyAdmin, MsgUpdateGroupPolicyDecisionPolicy, MsgUpdateGroupPolicyMetadata, MsgVote } from "./group/v1/tx"; import { AirdropAminoConverter } from "../features/airdrop/amino"; import { MsgUnjail } from "./slashing/tx"; import { SlashingAminoConverter } from "../features/slashing/slashing"; @@ -44,6 +44,360 @@ export async function signAndBroadcastGroupMsg( return await client.signAndBroadcast(signer, msgs, fee, memo); } +export async function signAndBroadcastUpdateGroupMembers( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupMembers", + MsgUpdateGroupMembers + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastUpdateGroupPolicy( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupPolicyDecisionPolicy", + MsgUpdateGroupPolicyDecisionPolicy + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastUpdateGroupPolicyMetadata( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupPolicyMetadata", + MsgUpdateGroupPolicyMetadata + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastUpdateGroupPolicyAdmin( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupPolicyAdmin", + MsgUpdateGroupPolicyAdmin + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastAddGroupPolicy( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgCreateGroupPolicy", + MsgCreateGroupPolicy + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastLeaveGroup( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + + registry.register( + "/cosmos.group.v1.MsgLeaveGroup", + MsgLeaveGroup + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastGroupProposalVote( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + const aTypes = new AminoTypes({ + ...MsgVote, + }); + + registry.register( + "/cosmos.group.v1.MsgVote", + MsgVote + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + aminoTypes: aTypes, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastGroupProposalExecute( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + const aTypes = new AminoTypes({ + ...MsgExec, + }); + + registry.register( + "/cosmos.group.v1.MsgExec", + MsgExec + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + aminoTypes: aTypes, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastGroupProposal( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + const aTypes = new AminoTypes({ + ...MsgSubmitProposal, + }); + + defaultRegistryTypes.forEach((v) => { + registry.register(v[0], v[1]); + }); + + registry.register( + "/cosmos.group.v1.MsgSubmitProposal", + MsgSubmitProposal + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + aminoTypes: aTypes, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastUpdateGroupAdmin( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + const aTypes = new AminoTypes({ + ...MsgUpdateGroupAdmin, + }); + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupAdmin", + MsgUpdateGroupAdmin + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + aminoTypes: aTypes, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + +export async function signAndBroadcastUpdateGroupMetadata( + signer, + msgs, + fee, + chainId, + rpcURL, + memo = "" +) { + await window.keplr.enable(chainId); + const offlineSigner = + window.getOfflineSigner && window.keplr.getOfflineSigner(chainId); + let registry = new Registry(); + + const aTypes = new AminoTypes({ + ...MsgUpdateGroupMetadata, + }); + + registry.register( + "/cosmos.group.v1.MsgUpdateGroupMetadata", + MsgUpdateGroupMetadata + ); + + const client = await SigningStargateClient.connectWithSigner( + rpcURL, + offlineSigner, + { + registry: registry, + aminoTypes: aTypes, + } + ); + + return await client.signAndBroadcast(signer, msgs, fee, memo); +} + export async function signAndBroadcastClaimMsg( signer, msgs, diff --git a/src/txns/group/group.ts b/src/txns/group/group.ts new file mode 100644 index 000000000..7d85aaba0 --- /dev/null +++ b/src/txns/group/group.ts @@ -0,0 +1,406 @@ +import { + MsgCreateGroup, MsgCreateGroupWithPolicy, + MsgVote, + MsgSubmitProposal, + MsgExec, + MsgUpdateGroupMembers, + MsgLeaveGroup, + MsgUpdateGroupPolicyDecisionPolicy, + MsgUpdateGroupAdmin, + MsgUpdateGroupMetadata, + MsgCreateGroupPolicy, + MsgUpdateGroupPolicyMetadata, + MsgUpdateGroupPolicyAdmin, +} from "./v1/tx"; +import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; +import { + MsgDelegate +} from "cosmjs-types/cosmos/staking/v1beta1/tx"; +import { Duration } from "cosmjs-types/google/protobuf/duration"; +import { + PercentageDecisionPolicy, ThresholdDecisionPolicy, + DecisionPolicyWindows, +} from "./v1/types"; +import Long from "long"; + +// group + +const msgCreateGroup = "/cosmos.group.v1.MsgCreateGroup"; +const msgCreateGroupWithPolicy = "/cosmos.group.v1.MsgCreateGroupWithPolicy"; +const msgCreateGroupWithThresholdPolicy = `/cosmos.group.v1.ThresholdDecisionPolicy`; +const msgCreateGroupWithPercentagePolicy = `/cosmos.group.v1.PercentageDecisionPolicy`; +const msgCreateGroupProposal = `/cosmos.group.v1.MsgSubmitProposal`; +const msgGroupProposalVote = `/cosmos.group.v1.MsgVote`; +const msgGroupProposalExecute = `/cosmos.group.v1.MsgExec`; +const msgUpdateGroupMember = `/cosmos.group.v1.MsgUpdateGroupMembers`; +const msgLeaveGroupMember = `/cosmos.group.v1.MsgLeaveGroup`; +const msgUpdateGroupPolicy = `/cosmos.group.v1.MsgUpdateGroupPolicyDecisionPolicy`; +const msgUpdateGroupAdmin = `/cosmos.group.v1.MsgUpdateGroupAdmin`; +const msgUpdateGroupMetadata = `/cosmos.group.v1.MsgUpdateGroupMetadata`; +const msgAddGroupPolicy = `/cosmos.group.v1.MsgCreateGroupPolicy`; +const msgUpatePolicyMetdata = `/cosmos.group.v1.MsgUpdateGroupPolicyMetadata`; +const msgUpatePolicyAdmin = `/cosmos.group.v1.MsgUpdateGroupPolicyAdmin`; + +export function CreateGroup(admin: any, metadata: any, members: any) { + try { + return { + typeUrl: msgCreateGroup, + value: MsgCreateGroup.fromPartial({ + admin: admin, + members: members, + metadata: metadata, + }), + }; + } catch (error) { + console.log('Error while creating obj for group and members', error) + throw error; + } + +} + +export function CreateProposalVote( + proposalId: string, + voter: string, + option: any, + metadata?: string +) { + return { + typeUrl: msgGroupProposalVote, + value: MsgVote.fromPartial({ + proposalId, + voter, + option: option, + metadata + }) + } +} + +export function CreateProposalExecute( + proposalId: string, + executor: string +) { + return { + typeUrl: msgGroupProposalExecute, + value: MsgExec.fromPartial({ + proposalId, + executor + }) + } +} + +export function CreateGroupProposal(groupPolicyAddress = '', + proposers: any = [], metadata = '', messages = []) { + var msgs: any = []; + let length = messages?.length || 0; + + for (let i = 0; i < length; i++) { + let msg = messages[i]; + let type = msg['typeUrl']; + switch (type) { + case '/cosmos.bank.v1beta1.MsgSend': + msgs = [...msgs, { + typeUrl: type, + value: MsgSend.encode(msg['value']).finish() + }] + break; + case '/cosmos.staking.v1beta1.MsgDelegate': + msgs = [...msgs, { + typeUrl: type, + value: MsgDelegate.encode(msg['value']).finish(), + }] + break; + } + } + + return { + typeUrl: msgCreateGroupProposal, + value: MsgSubmitProposal.fromPartial({ + groupPolicyAddress, + proposers, + metadata, + messages: msgs + }) + } +} + +export function CreateGroupWithPolicy( + admin: any, + groupMetadata: any, + members: any, + decisionPolicy: any, + policyMetadata: any, + policyAsAdmin: any = false +) { + console.log('policy metada--------', policyMetadata, members, admin, groupMetadata, policyAsAdmin) + + try { + + if (Object.keys(policyMetadata)?.length) { + console.log('here----') + const obj = { + typeUrl: msgCreateGroupWithPolicy, + value: MsgCreateGroupWithPolicy.fromPartial({ + admin: admin, + members: members, + groupMetadata: groupMetadata, + groupPolicyAsAdmin: policyAsAdmin, + groupPolicyMetadata: policyMetadata, + decisionPolicy: { + typeUrl: policyMetadata?.percentage && msgCreateGroupWithPercentagePolicy || + msgCreateGroupWithThresholdPolicy, + value: policyMetadata?.percentage && + PercentageDecisionPolicy.encode({ + percentage: parseFloat(policyMetadata?.percentage || 0).toFixed(2).toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() || + ThresholdDecisionPolicy.encode({ + threshold: policyMetadata?.threshold?.toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() + }, + }), + }; + + return obj; + } else { + const obj = { + typeUrl: msgCreateGroupWithPolicy, + value: MsgCreateGroupWithPolicy.fromPartial({ + admin: admin, + members: members, + decisionPolicy: {}, + groupMetadata: groupMetadata, + groupPolicyAsAdmin: policyAsAdmin, + groupPolicyMetadata: policyMetadata, + }), + }; + + return obj; + } + } catch (error) { + console.log('Error while creating the obj -- ', error) + throw error; + } +} + +export function UpdateGroupMembers( + admin: any, + members: any, + groupId: any, +) { + + const obj = { + typeUrl: msgUpdateGroupMember, + value: MsgUpdateGroupMembers.fromPartial({ + admin: admin, + memberUpdates: members, + groupId: groupId, + }), + }; + + return obj; +} + +export function UpdateGroupAdmin( + admin: any, + groupId: any, + newAdmin: any, +) { + + const obj = { + typeUrl: msgUpdateGroupAdmin, + value: MsgUpdateGroupAdmin.fromPartial({ + admin, + groupId, + newAdmin, + }) + }; + + return obj; +} + +export function UpdateGroupMetadata( + admin: any, + groupId: any, + metadata: any, +) { + + const obj = { + typeUrl: msgUpdateGroupMetadata, + value: MsgUpdateGroupMetadata.fromPartial({ + admin, + groupId, + metadata, + }) + }; + + return obj; +} + + +export function CreateGroupPolicy( + admin: any, + groupId: any, + policyMetadata: any, +) { + + const obj = { + typeUrl: msgAddGroupPolicy, + value: MsgCreateGroupPolicy.fromPartial({ + admin: admin, + groupId, + decisionPolicy: { + typeUrl: policyMetadata?.percentage && msgCreateGroupWithPercentagePolicy || + msgCreateGroupWithThresholdPolicy, + value: policyMetadata?.percentage && + PercentageDecisionPolicy.encode({ + percentage: parseFloat(policyMetadata?.percentage || 0).toFixed(2).toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() || + ThresholdDecisionPolicy.encode({ + threshold: policyMetadata?.threshold?.toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() + }, + }), + }; + + return obj; +} + +export function UpdateGroupPolicy( + admin: any, + groupPolicyAddress: any, + policyMetadata: any, +) { + + const obj = { + typeUrl: msgUpdateGroupPolicy, + value: MsgUpdateGroupPolicyDecisionPolicy.fromPartial({ + admin: admin, + groupPolicyAddress: groupPolicyAddress, + decisionPolicy: { + typeUrl: policyMetadata?.percentage && msgCreateGroupWithPercentagePolicy || + msgCreateGroupWithThresholdPolicy, + value: policyMetadata?.percentage && + PercentageDecisionPolicy.encode({ + percentage: parseFloat(policyMetadata?.percentage || 0).toFixed(2).toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() || + ThresholdDecisionPolicy.encode({ + threshold: policyMetadata?.threshold?.toString(), + windows: DecisionPolicyWindows.fromPartial({ + votingPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.votingPeriod), + nanos: Number(policyMetadata?.votingPeriod) + }), + minExecutionPeriod: Duration.fromPartial({ + seconds: Long.fromNumber(policyMetadata?.minExecPeriod), + nanos: Number(policyMetadata?.minExecPeriod) + }), + }) + }).finish() + }, + }), + }; + + return obj; +} + +export function CreateLeaveGroupMember( + memberAddress: any, + groupId: any, +) { + + const obj = { + typeUrl: msgLeaveGroupMember, + value: MsgLeaveGroup.fromPartial({ + groupId: groupId, + address: memberAddress + }), + }; + + return obj; +} + +export function UpdatePolicyMetadata( + admin: any, + address: any, + metadata: any +) { + + const obj = { + typeUrl: msgUpatePolicyMetdata, + value: MsgUpdateGroupPolicyMetadata.fromPartial({ + admin: admin, + groupPolicyAddress: address, + metadata: metadata + }), + }; + + return obj; +} + +export function UpdatePolicyAdmin( + admin: any, + address: any, + newAdmin: any +) { + + const obj = { + typeUrl: msgUpatePolicyAdmin, + value: MsgUpdateGroupPolicyAdmin.fromPartial({ + admin: admin, + groupPolicyAddress: address, + newAdmin: newAdmin + }), + }; + + return obj; +} + diff --git a/src/txns/types.tsx b/src/txns/types.tsx index 64d2bbd7e..dfe362359 100644 --- a/src/txns/types.tsx +++ b/src/txns/types.tsx @@ -1,5 +1,5 @@ export interface Msg { - typeUrl: string - value: any + typeUrl?: string + value?: any } \ No newline at end of file diff --git a/src/utils/util.js b/src/utils/util.js index effe8ea42..8634d15a1 100644 --- a/src/utils/util.js +++ b/src/utils/util.js @@ -1,4 +1,6 @@ import Chip from "@mui/material/Chip"; +import { Decimal } from "@cosmjs/math"; + export function getTypeURLName(url) { if (!url) { @@ -201,3 +203,86 @@ export function mainValueToMinimum(amount, coinInfo) { export function amountToMinimalValue(amount, coinInfo) { return Number(amount) * 10 ** coinInfo.coinDecimals; } + +export const setLocalStorage = (key, value, type) => { + if (type === 'object') { + localStorage.setItem(key, JSON.stringify(value)) + return + } else { + localStorage.setItem(key, value); + return + } +} + +export const getLocalStorage = (key, type) => { + if (type === 'object') { + let obj = JSON.parse(localStorage.getItem(key)); + return obj + } else { + let value = localStorage.getItem(key) + return value; + } +} + +export const getAmountObj = (amount = 0, chainInfo) => { + const amountInAtomics = Decimal.fromUserInput( + amount, + Number(chainInfo.config.currencies[0].coinDecimals) + ).atomics; + + return { + amount: amountInAtomics, + denom: chainInfo.config.currencies[0].coinMinimalDenom + } +} + +export const proposalStatus = { + 'PROPOSAL_EXECUTOR_RESULT_NOT_RUN': { + label: 'Result not run', + bgColor: '#e3bbbb', + textColor: '#ad3131', + color: 'error' + }, + 'PROPOSAL_STATUS_UNSPECIFIED': { + label: 'Unspecified', + bgColor: '#e3bbbb', + textColor: '#ad3131', + color: 'error' + }, + 'PROPOSAL_STATUS_SUBMITTED': { + label: 'Submitted', + bgColor: '#c5c9e3', + textColor: '#3049d3', + color: 'primary' + }, 'PROPOSAL_STATUS_ACCEPTED': { + label: 'Accepted', + bgColor: '#d8dfd3', + textColor: '#30b448', + color: 'success' + }, 'PROPOSAL_STATUS_REJECTED': { + label: 'Rejected', + bgColor: '#c5c9e3', + textColor: '#3049d3', + color: 'error' + }, 'PROPOSAL_STATUS_ABORTED': { + label: 'Aborted', + bgColor: '#c5c9e3', + textColor: '#3049d3', + color: 'error' + }, + 'PROPOSAL_STATUS_WITHDRAWN': { + label: 'Withdrawn', + bgColor: '#e7d4ca', + textColor: '#e56a11' + }, +} + +export const ThresholdDecisionPolicy = `/cosmos.group.v1.ThresholdDecisionPolicy`; +export const PercentageDecisionPolicy = `/cosmos.group.v1.PercentageDecisionPolicy`; + +export const PoliciesTypes = { + '/cosmos.group.v1.PercentageDecisionPolicy': 'Percentage Decision Policy', + '/cosmos.group.v1.ThresholdDecisionPolicy': 'Threshold Decision Policy' +} + +