From 27a8dd3851bdd457c2e77f19a837c92958df02fb Mon Sep 17 00:00:00 2001
From: Manank Patni <manank321@gmail.com>
Date: Sat, 21 Jan 2023 18:48:36 +0530
Subject: [PATCH 1/3] Add Unstake from all proposals button

Signed-off-by: Manank Patni <manank321@gmail.com>
---
 .../explorer/hooks/useCanDropProposal.ts      |  6 ++-
 .../explorer/pages/ProposalDetails/index.tsx  | 30 +++++------
 src/modules/explorer/pages/User/index.tsx     | 50 +++++++++++++++--
 src/services/contracts/baseDAO/class.ts       | 11 ++++
 .../dao/hooks/useUnstakeFromAllProposals.ts   | 54 +++++++++++++++++++
 .../indexer/dao/mappers/proposal/types.ts     |  4 +-
 src/services/indexer/dao/queries.ts           |  2 +
 src/services/indexer/types.ts                 |  1 +
 8 files changed, 137 insertions(+), 21 deletions(-)
 create mode 100644 src/services/indexer/dao/hooks/useUnstakeFromAllProposals.ts

diff --git a/src/modules/explorer/hooks/useCanDropProposal.ts b/src/modules/explorer/hooks/useCanDropProposal.ts
index a848d172..17e25cf7 100644
--- a/src/modules/explorer/hooks/useCanDropProposal.ts
+++ b/src/modules/explorer/hooks/useCanDropProposal.ts
@@ -20,12 +20,14 @@ export const useCanDropProposal = (daoId: string, proposalId: string) => {
 
     const isGuardian = dao.data.guardian.toLowerCase() === account.toLowerCase()
 
-    const isNotExecutedOrDropped = true
+    const isNotExecutedOrDropped =
+      (cycleInfo && proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.DROPPED) ||
+      (cycleInfo && proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.EXECUTED)
 
     // dao.data.proposalsToFlush.find(
     //   (id) => id.toLowerCase() === proposal.id.toLowerCase()
     // );
 
-    return isNotExecutedOrDropped && (isProposer || hasExpired || isGuardian)
+    return !isNotExecutedOrDropped && (isProposer || hasExpired || isGuardian)
   }, [account, cycleInfo, dao, proposal])
 }
diff --git a/src/modules/explorer/pages/ProposalDetails/index.tsx b/src/modules/explorer/pages/ProposalDetails/index.tsx
index b087b903..f717567b 100644
--- a/src/modules/explorer/pages/ProposalDetails/index.tsx
+++ b/src/modules/explorer/pages/ProposalDetails/index.tsx
@@ -127,12 +127,12 @@ export const ProposalDetails: React.FC = () => {
     })
   }, [dao, dropProposal, proposalId])
 
-  const onUnstakeVotes = useCallback(async () => {
-    await mutateUnstake({
-      dao: dao as BaseDAO,
-      proposalId
-    })
-  }, [dao, mutateUnstake, proposalId])
+  // const onUnstakeVotes = useCallback(async () => {
+  //   await mutateUnstake({
+  //     dao: dao as BaseDAO,
+  //     proposalId
+  //   })
+  // }, [dao, mutateUnstake, proposalId])
 
   const proposalCycle = proposal ? proposal.period : "-"
 
@@ -160,13 +160,13 @@ export const ProposalDetails: React.FC = () => {
 
   const canVote = cycleInfo && proposal?.getStatus(cycleInfo.currentLevel).status === ProposalStatus.ACTIVE
 
-  const canUnstakeVotes =
-    cycleInfo &&
-    proposal &&
-    account &&
-    (proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.DROPPED ||
-      proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.EXECUTED) &&
-    proposal.voters.some(({ address }) => address.toLowerCase() === account.toLowerCase())
+  // const canUnstakeVotes =
+  //   cycleInfo &&
+  //   proposal &&
+  //   account &&
+  //   (proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.DROPPED ||
+  //     proposal.getStatus(cycleInfo.currentLevel).status === ProposalStatus.EXECUTED) &&
+  //   proposal.voters.some(({ address }) => address.toLowerCase() === account.toLowerCase())
 
   const parseReadableConfigValue = (configKey: keyof Proposal["metadata"]["config"], value: BigNumber) => {
     if (dao) {
@@ -204,7 +204,7 @@ export const ProposalDetails: React.FC = () => {
                   <InfoIcon color="secondary" />
                 </Tooltip>
               </Grid>
-              <Grid>
+              {/* <Grid>
                 <Button variant="contained" color="secondary" disabled={!canUnstakeVotes} onClick={onUnstakeVotes}>
                   Unstake votes
                 </Button>
@@ -214,7 +214,7 @@ export const ProposalDetails: React.FC = () => {
                 >
                   <InfoIcon color="secondary" />
                 </Tooltip>
-              </Grid>
+              </Grid> */}
             </Grid>
             <Grid item>
               <Grid container justifyContent="space-between" alignItems="center">
diff --git a/src/modules/explorer/pages/User/index.tsx b/src/modules/explorer/pages/User/index.tsx
index c886c4b7..0a988d24 100644
--- a/src/modules/explorer/pages/User/index.tsx
+++ b/src/modules/explorer/pages/User/index.tsx
@@ -1,7 +1,7 @@
 import { Box, Grid, Theme, Typography } from "@material-ui/core"
 import { styled } from "@material-ui/styles"
 import dayjs from "dayjs"
-import React, { useEffect, useMemo } from "react"
+import React, { useCallback, useEffect, useMemo } from "react"
 import { useHistory } from "react-router"
 import { useAgoraTopic } from "services/agora/hooks/useTopic"
 import { useTezos } from "services/beacon/hooks/useTezos"
@@ -16,6 +16,8 @@ import { FreezeDialog } from "../../components/FreezeDialog"
 import { UserBalances } from "../../components/UserBalances"
 import { StatusBadge } from "../../components/StatusBadge"
 import { ProposalsList } from "../../components/ProposalsList"
+import { useUnstakeFromAllProposals } from "services/indexer/dao/hooks/useUnstakeFromAllProposals"
+import { DropButton } from "../Proposals"
 
 const ContentBlockItem = styled(Grid)({
   padding: "35px 52px",
@@ -36,7 +38,7 @@ const MainContainer = styled(Box)({
 })
 
 const UsernameText = styled(Typography)({
-  fontSize: 28,
+  fontSize: 18,
   wordBreak: "break-all"
 })
 
@@ -94,10 +96,14 @@ export const ProposalItem: React.FC<{
 export const User: React.FC = () => {
   const { account } = useTezos()
   const daoId = useDAOID()
-  const { cycleInfo } = useDAO(daoId)
+  const { data, cycleInfo } = useDAO(daoId)
   const { data: proposals } = useProposals(daoId)
   const history = useHistory()
 
+  const { data: executedProposals } = useProposals(daoId, ProposalStatus.EXECUTED)
+  const { data: droppedProposals } = useProposals(daoId, ProposalStatus.DROPPED)
+  const { mutate: unstakeFromAllProposals } = useUnstakeFromAllProposals()
+
   useEffect(() => {
     if (!account) {
       history.push(`../${daoId}`)
@@ -120,6 +126,34 @@ export const User: React.FC = () => {
     return proposals.filter(p => p.voters.map(voter => voter.address.toLowerCase()).includes(account.toLowerCase()))
   }, [account, proposals])
 
+  const onUnstakeFromAllProposals = useCallback(async () => {
+    if (droppedProposals && executedProposals && data) {
+      const allProposals = droppedProposals.concat(executedProposals)
+
+      const proposalsWithStakedTokens: Proposal[] = []
+
+      allProposals.forEach((proposal: Proposal) => {
+        const userVote = proposal.voters.find(voter => voter.address === account)
+        if (userVote && userVote.staked) {
+          proposalsWithStakedTokens.push(proposal)
+        }
+      })
+
+      unstakeFromAllProposals({
+        dao: data,
+        allProposals: proposalsWithStakedTokens.map(p => p.id)
+      })
+      return
+    }
+  }, [data, account, unstakeFromAllProposals, droppedProposals, executedProposals])
+
+  const canUnstakeVotes: boolean | undefined =
+    executedProposals &&
+    droppedProposals &&
+    executedProposals
+      .concat(droppedProposals)
+      .some(proposal => proposal.voters.find(vote => vote.address === account)?.staked)
+
   const getVoteDecision = (proposal: Proposal) =>
     proposal.voters.find(voter => voter.address.toLowerCase())?.support as boolean
 
@@ -150,6 +184,16 @@ export const User: React.FC = () => {
                     <Grid item>
                       <FreezeDialog freeze={false} />
                     </Grid>
+                    <Grid item>
+                      <DropButton
+                        variant="contained"
+                        color="secondary"
+                        onClick={onUnstakeFromAllProposals}
+                        disabled={!canUnstakeVotes}
+                      >
+                        Unstake Votes
+                      </DropButton>
+                    </Grid>
                   </Grid>
                 </Grid>
               </Grid>
diff --git a/src/services/contracts/baseDAO/class.ts b/src/services/contracts/baseDAO/class.ts
index 0d6043bf..09db7d51 100644
--- a/src/services/contracts/baseDAO/class.ts
+++ b/src/services/contracts/baseDAO/class.ts
@@ -135,6 +135,17 @@ export abstract class BaseDAO {
     return await batch.send()
   }
 
+  public unstakeFromAllProposals = async (proposals: string[], account: string, tezos: TezosToolkit) => {
+    const daoContract = await getContract(tezos, this.data.address)
+    const initialBatch = await tezos.wallet.batch()
+
+    const batch = proposals.reduce((prev, current) => {
+      return prev.withContractCall(daoContract.methods.unstake_vote([current]))
+    }, initialBatch)
+
+    return await batch.send()
+  }
+
   public sendXtz = async (xtzAmount: BigNumber, tezos: TezosToolkit) => {
     const contract = await getContract(tezos, this.data.address)
 
diff --git a/src/services/indexer/dao/hooks/useUnstakeFromAllProposals.ts b/src/services/indexer/dao/hooks/useUnstakeFromAllProposals.ts
new file mode 100644
index 00000000..01ab0f4a
--- /dev/null
+++ b/src/services/indexer/dao/hooks/useUnstakeFromAllProposals.ts
@@ -0,0 +1,54 @@
+import { useNotification } from "modules/common/hooks/useNotification"
+import { useMutation, useQueryClient } from "react-query"
+import { useTezos } from "services/beacon/hooks/useTezos"
+import { BaseDAO } from "services/contracts/baseDAO"
+import { networkNameMap } from "../../../bakingBad"
+
+export const useUnstakeFromAllProposals = () => {
+  const queryClient = useQueryClient()
+  const openNotification = useNotification()
+  const { network, tezos, account, connect } = useTezos()
+
+  return useMutation<any | Error, Error, { dao: BaseDAO; allProposals: string[] }>(
+    async params => {
+      const { key: unstakeNotification, closeSnackbar: closeFlushNotification } = openNotification({
+        message: "Please sign the transaction to unstake tokens from all proposals",
+        persist: true,
+        variant: "info"
+      })
+      try {
+        let tezosToolkit = tezos
+
+        if (!account) {
+          tezosToolkit = await connect()
+        }
+
+        const data = await params.dao.unstakeFromAllProposals(params.allProposals, account, tezosToolkit)
+        closeFlushNotification(unstakeNotification)
+
+        await data.confirmation(1)
+        openNotification({
+          message: "Execute transaction confirmed!",
+          autoHideDuration: 5000,
+          variant: "success",
+          detailsLink: `https://${networkNameMap[network]}.tzkt.io/` + data.opHash
+        })
+
+        return data
+      } catch (e) {
+        closeFlushNotification(unstakeNotification)
+        openNotification({
+          message: "An error has happened with execute transaction!",
+          variant: "error",
+          autoHideDuration: 5000
+        })
+        return new Error((e as Error).message)
+      }
+    },
+    {
+      onSuccess: () => {
+        queryClient.resetQueries()
+      }
+    }
+  )
+}
diff --git a/src/services/indexer/dao/mappers/proposal/types.ts b/src/services/indexer/dao/mappers/proposal/types.ts
index 2b17ec73..d5b83a42 100644
--- a/src/services/indexer/dao/mappers/proposal/types.ts
+++ b/src/services/indexer/dao/mappers/proposal/types.ts
@@ -64,6 +64,7 @@ export abstract class Proposal {
     address: string
     value: BigNumber
     support: boolean
+    staked: boolean
   }[]
   type: DAOTemplate
 
@@ -87,7 +88,8 @@ export abstract class Proposal {
     this.voters = dto.votes.map(vote => ({
       address: vote.holder.address,
       value: parseUnits(new BigNumber(vote.amount), this.dao.data.token.decimals),
-      support: Boolean(vote.support)
+      support: Boolean(vote.support),
+      staked: vote.staked
     }))
     this.upVotes = this.voters.reduce((acc, voter) => {
       if (voter.support) {
diff --git a/src/services/indexer/dao/queries.ts b/src/services/indexer/dao/queries.ts
index 7d32fec6..2508a0b9 100644
--- a/src/services/indexer/dao/queries.ts
+++ b/src/services/indexer/dao/queries.ts
@@ -163,6 +163,7 @@ export const GET_PROPOSALS_QUERY = gql`
           }
           id
           support
+          staked
         }
       }
     }
@@ -206,6 +207,7 @@ export const GET_PROPOSAL_QUERY = gql`
           }
           id
           support
+          staked
         }
       }
     }
diff --git a/src/services/indexer/types.ts b/src/services/indexer/types.ts
index cdc9a27e..9cf257fb 100644
--- a/src/services/indexer/types.ts
+++ b/src/services/indexer/types.ts
@@ -166,6 +166,7 @@ export interface VoteDTO {
   amount: string
   support: boolean
   holder: HolderDTO
+  staked: boolean
 }
 
 export interface DAOListItem {

From b4927480e63e425349c7a9d4b1782daac8eba060 Mon Sep 17 00:00:00 2001
From: Manank Patni <manank321@gmail.com>
Date: Sat, 21 Jan 2023 19:41:51 +0530
Subject: [PATCH 2/3] Change Hasura ENV Variables key

Signed-off-by: Manank Patni <manank321@gmail.com>
---
 src/services/config/constants.ts | 6 +++---
 src/services/indexer/graphql.ts  | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/services/config/constants.ts b/src/services/config/constants.ts
index 9f1e2ed8..7438167f 100644
--- a/src/services/config/constants.ts
+++ b/src/services/config/constants.ts
@@ -1,14 +1,14 @@
 export enum EnvKey {
   REACT_APP_ENV = "REACT_APP_ENV",
   REACT_APP_NETWORK = "REACT_APP_NETWORK",
-  REACT_APP_HASURA_URL = "REACT_APP_HASURA_URL",
-  REACT_APP_HASURA_ADMIN_SECRET = "REACT_APP_HASURA_ADMIN_SECRET",
   REACT_APP_CORS_PROXY_URL = "REACT_APP_CORS_PROXY_URL",
   REACT_APP_MIXPANEL_TOKEN = "REACT_APP_MIXPANEL_TOKEN",
   REACT_APP_MIXPANEL_DEBUG_ENABLED = "REACT_APP_MIXPANEL_DEBUG_ENABLED",
   REACT_APP_LAUNCH_DARKLY_SDK_DEV = "REACT_APP_LAUNCH_DARKLY_SDK_DEV",
   REACT_APP_LAUNCH_DARKLY_SDK_PROD = "REACT_APP_LAUNCH_DARKLY_SDK_PROD",
-  REACT_APP_URL = "REACT_APP_URL"
+  REACT_APP_URL = "REACT_APP_URL",
+  REACT_APP_HASURA_URL_V2 = "REACT_APP_HASURA_URL_V2",
+  REACT_APP_HASURA_ADMIN_SECRET_V2 = "REACT_APP_HASURA_ADMIN_SECRET_V2"
 }
 
 export enum FeatureFlag {
diff --git a/src/services/indexer/graphql.ts b/src/services/indexer/graphql.ts
index b4bf933c..97f7cc42 100644
--- a/src/services/indexer/graphql.ts
+++ b/src/services/indexer/graphql.ts
@@ -1,8 +1,8 @@
 import { GraphQLClient } from "graphql-request"
 import { EnvKey, getEnv } from "services/config"
 
-const BASE_URL = getEnv(EnvKey.REACT_APP_HASURA_URL)
-const HASURA_ADMIN_SECRET = getEnv(EnvKey.REACT_APP_HASURA_ADMIN_SECRET)
+const BASE_URL = getEnv(EnvKey.REACT_APP_HASURA_URL_V2)
+const HASURA_ADMIN_SECRET = getEnv(EnvKey.REACT_APP_HASURA_ADMIN_SECRET_V2)
 
 if (!BASE_URL) {
   throw new Error(`${EnvKey.REACT_APP_HASURA_URL} env variable is missing`)

From 608dfd6bac103ab13a77e3f542b9e0c84e648bb2 Mon Sep 17 00:00:00 2001
From: Manank Patni <manank321@gmail.com>
Date: Sat, 21 Jan 2023 19:49:35 +0530
Subject: [PATCH 3/3] Change Hasura ENV Variables key - Bug Fix

Signed-off-by: Manank Patni <manank321@gmail.com>
---
 src/services/indexer/graphql.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/services/indexer/graphql.ts b/src/services/indexer/graphql.ts
index 97f7cc42..e7bffbb6 100644
--- a/src/services/indexer/graphql.ts
+++ b/src/services/indexer/graphql.ts
@@ -5,11 +5,11 @@ const BASE_URL = getEnv(EnvKey.REACT_APP_HASURA_URL_V2)
 const HASURA_ADMIN_SECRET = getEnv(EnvKey.REACT_APP_HASURA_ADMIN_SECRET_V2)
 
 if (!BASE_URL) {
-  throw new Error(`${EnvKey.REACT_APP_HASURA_URL} env variable is missing`)
+  throw new Error(`${EnvKey.REACT_APP_HASURA_URL_V2} env variable is missing`)
 }
 
 if (!HASURA_ADMIN_SECRET) {
-  throw new Error(`${EnvKey.REACT_APP_HASURA_ADMIN_SECRET} env variable is missing`)
+  throw new Error(`${EnvKey.REACT_APP_HASURA_ADMIN_SECRET_V2} env variable is missing`)
 }
 
 export const client = new GraphQLClient(BASE_URL, {
