From db3003338fd7024944a37015472c39a51b3526b2 Mon Sep 17 00:00:00 2001 From: Manank Patni Date: Sun, 16 Jul 2023 04:34:42 +0530 Subject: [PATCH] Add interaction with contracts Signed-off-by: Manank Patni --- .../explorer/components/UserBalances.tsx | 10 ++- .../User/components/DelegationBanner.tsx | 49 +++++++++++--- .../pages/User/components/DelegationModal.tsx | 49 ++++++++++---- src/modules/explorer/pages/User/index.tsx | 5 +- src/services/bakingBad/delegations/index.ts | 64 ++++++++++++++++++- src/services/bakingBad/delegations/types.ts | 16 +++++ .../token/hooks/useDelegationStatus.ts | 27 ++++++++ .../token/hooks/useDelegationVoteWeight.ts | 25 ++++++++ .../contracts/token/hooks/useTokenDelegate.ts | 63 ++++++++++++++++++ src/services/contracts/token/index.ts | 19 ++++++ 10 files changed, 297 insertions(+), 30 deletions(-) create mode 100644 src/services/contracts/token/hooks/useDelegationStatus.ts create mode 100644 src/services/contracts/token/hooks/useDelegationVoteWeight.ts create mode 100644 src/services/contracts/token/hooks/useTokenDelegate.ts diff --git a/src/modules/explorer/components/UserBalances.tsx b/src/modules/explorer/components/UserBalances.tsx index 3df01b86..6022b413 100644 --- a/src/modules/explorer/components/UserBalances.tsx +++ b/src/modules/explorer/components/UserBalances.tsx @@ -75,7 +75,7 @@ const BalanceToken = styled(Typography)({ fontWeight: 300 }) -export const UserBalances: React.FC<{ daoId: string; setVoteWeight: any }> = ({ daoId, children, setVoteWeight }) => { +export const UserBalances: React.FC<{ daoId: string }> = ({ daoId, children }) => { const { account } = useTezos() const { data: dao, ledger } = useDAO(daoId) const theme = useTheme() @@ -104,7 +104,6 @@ export const UserBalances: React.FC<{ daoId: string; setVoteWeight: any }> = ({ userBalances.available.balance = "-" userBalances.pending.balance = "-" userBalances.staked.balance = "-" - setVoteWeight("-") return userBalances } @@ -112,9 +111,8 @@ export const UserBalances: React.FC<{ daoId: string; setVoteWeight: any }> = ({ userBalances.available.balance = userLedger.available_balance.dp(10, 1).toString() userBalances.pending.balance = userLedger.pending_balance.dp(10, 1).toString() userBalances.staked.balance = userLedger.staked.dp(10, 1).toString() - setVoteWeight(userBalances.staked.balance) return userBalances - }, [account, ledger, setVoteWeight]) + }, [account, ledger]) const balancesList = Object.keys(balances).map(key => balances[key as keyof Balances]) @@ -148,10 +146,10 @@ export const UserBalances: React.FC<{ daoId: string; setVoteWeight: any }> = ({ ) } -export const UserBalancesBox: React.FC<{ daoId: string; setVoteWeight: any }> = ({ daoId, setVoteWeight }) => { +export const UserBalancesBox: React.FC<{ daoId: string }> = ({ daoId }) => { return ( - + ) } diff --git a/src/modules/explorer/pages/User/components/DelegationBanner.tsx b/src/modules/explorer/pages/User/components/DelegationBanner.tsx index 003447f4..dcc3775a 100644 --- a/src/modules/explorer/pages/User/components/DelegationBanner.tsx +++ b/src/modules/explorer/pages/User/components/DelegationBanner.tsx @@ -3,6 +3,11 @@ import { Grid, Theme, Typography, styled } from "@material-ui/core" import { useDAO } from "services/services/dao/hooks/useDAO" import { Edit } from "@material-ui/icons" import { DelegationDialog } from "./DelegationModal" +import { useDelegationStatus } from "services/contracts/token/hooks/useDelegationStatus" +import { useTezos } from "services/beacon/hooks/useTezos" +import { useDelegationVoteWeight } from "services/contracts/token/hooks/useDelegationVoteWeight" +import BigNumber from "bignumber.js" +import { parseUnits } from "services/contracts/utils" export enum DelegationsType { ACCEPTING_DELEGATION = "ACCEPTING_DELEGATION", @@ -44,20 +49,39 @@ export const matchTextToStatus = (value: DelegationsType | undefined) => { } } -export const Delegation: React.FC<{ voteWeight: any; daoId: string }> = ({ voteWeight, daoId }) => { +export const Delegation: React.FC<{ daoId: string }> = ({ daoId }) => { const { data: dao } = useDAO(daoId) - const [delegationStatus, setDelegationStatus] = useState( - DelegationsType.NOT_ACCEPTING_DELEGATION - ) + const { network, tezos, account, connect } = useTezos() + + const { data: delegatedTo } = useDelegationStatus(dao?.data.token.contract) + const [delegationStatus, setDelegationStatus] = useState(DelegationsType.NOT_ACCEPTING_DELEGATION) const [openModal, setOpenModal] = useState(false) + const { data: delegateVoteBalances } = useDelegationVoteWeight(dao?.data.token.contract) + const [voteWeight, setVoteWeight] = useState(new BigNumber(0)) + console.log("voteWeight: ", voteWeight.toString()) const onCloseAction = () => { setOpenModal(false) } useEffect(() => { - console.log("se actualizó", delegationStatus) - }, [delegationStatus]) + if (delegatedTo === account) { + setDelegationStatus(DelegationsType.ACCEPTING_DELEGATION) + } else if (delegatedTo && delegatedTo !== account) { + setDelegationStatus(DelegationsType.DELEGATING) + } else { + setDelegationStatus(DelegationsType.NOT_ACCEPTING_DELEGATION) + } + }, [delegatedTo, account]) + + useEffect(() => { + let totalVoteWeight = new BigNumber(0) + delegateVoteBalances?.forEach(delegatedVote => { + const balance = new BigNumber(delegatedVote.balance) + totalVoteWeight = totalVoteWeight.plus(balance) + }) + setVoteWeight(totalVoteWeight) + }, [delegateVoteBalances]) return ( @@ -71,7 +95,11 @@ export const Delegation: React.FC<{ voteWeight: any; daoId: string }> = ({ voteW Voting Weight - {voteWeight} {voteWeight !== "-" ? dao.data.token.symbol : ""} + {!voteWeight || voteWeight.eq(new BigNumber(0)) ? ( + "-" + ) : ( + <>{`${parseUnits(voteWeight, dao.data.token.decimals).toString()} ${dao.data.token.symbol}`} + )} )} @@ -95,13 +123,18 @@ export const Delegation: React.FC<{ voteWeight: any; daoId: string }> = ({ voteW - {matchTextToStatus(delegationStatus)} + + {matchTextToStatus(delegationStatus)} + {delegationStatus === DelegationsType.DELEGATING ? delegatedTo : null} + ) diff --git a/src/modules/explorer/pages/User/components/DelegationModal.tsx b/src/modules/explorer/pages/User/components/DelegationModal.tsx index b2859e71..d5b10b82 100644 --- a/src/modules/explorer/pages/User/components/DelegationModal.tsx +++ b/src/modules/explorer/pages/User/components/DelegationModal.tsx @@ -3,6 +3,10 @@ import React, { useEffect, useState } from "react" import { DelegationsType, matchTextToStatus } from "./DelegationBanner" import { ResponsiveDialog } from "modules/explorer/components/ResponsiveDialog" import { SmallButton } from "modules/common/SmallButton" +import { useTokenDelegate } from "services/contracts/token/hooks/useTokenDelegate" +import { useDAO } from "services/services/dao/hooks/useDAO" +import { useDAOID } from "../../DAO/router" +import { useTezos } from "services/beacon/hooks/useTezos" const AddressTextField = styled(TextField)({ "backgroundColor": "#2f3438", @@ -50,10 +54,17 @@ export const DelegationDialog: React.FC<{ open: boolean onClose: () => void status: DelegationsType | undefined - setDelegationStatus: (value: DelegationsType | undefined) => void -}> = ({ status, onClose, open, setDelegationStatus }) => { + setDelegationStatus: (value: DelegationsType) => void + delegationStatus: DelegationsType + delegatedTo: string | null | undefined +}> = ({ status, onClose, open, setDelegationStatus, delegationStatus, delegatedTo }) => { const [options, setOptions] = useState([]) const [selectedOption, setSelectedOption] = useState() + const { mutate: delegateToken } = useTokenDelegate() + const daoId = useDAOID() + const { data, cycleInfo } = useDAO(daoId) + const { tezos, connect, network, account } = useTezos() + const [newDelegate, setNewDelegate] = useState("") useEffect(() => { getOptionsByStatus(status) @@ -71,13 +82,20 @@ export const DelegationDialog: React.FC<{ const updateStatus = () => { if (selectedOption === ActionTypes.DELEGATE || selectedOption === ActionTypes.CHANGE_DELEGATE) { - return setDelegationStatus(DelegationsType.DELEGATING) - } - if (selectedOption === ActionTypes.STOP_ACCEPTING_DELEGATIONS || ActionTypes.STOP_DELEGATING) { - return setDelegationStatus(DelegationsType.NOT_ACCEPTING_DELEGATION) - } - if (selectedOption === ActionTypes.ACCEPT_DELEGATIONS) { - return setDelegationStatus(DelegationsType.ACCEPTING_DELEGATION) + if (newDelegate && data?.data.token.contract) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: newDelegate }) + } + } else if ( + selectedOption === ActionTypes.STOP_ACCEPTING_DELEGATIONS || + selectedOption === ActionTypes.STOP_DELEGATING + ) { + if (data?.data.token.contract) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: null }) + } + } else if (selectedOption === ActionTypes.ACCEPT_DELEGATIONS) { + if (data?.data.token.contract && account) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: account }) + } } } @@ -88,7 +106,7 @@ export const DelegationDialog: React.FC<{ setOptions(optionsOne) break case DelegationsType.ACCEPTING_DELEGATION: - const optionsTwo = [ActionTypes.STOP_ACCEPTING_DELEGATIONS, ActionTypes.DELEGATE] + const optionsTwo = [ActionTypes.STOP_ACCEPTING_DELEGATIONS] setOptions(optionsTwo) break case DelegationsType.DELEGATING: @@ -104,7 +122,7 @@ export const DelegationDialog: React.FC<{ Current Status - {matchTextToStatus(status)} + {matchTextToStatus(status)} {delegationStatus === DelegationsType.DELEGATING ? delegatedTo : null} @@ -130,7 +148,14 @@ export const DelegationDialog: React.FC<{ /> {item === selectedOption && (selectedOption === ActionTypes.DELEGATE || selectedOption === ActionTypes.CHANGE_DELEGATE) ? ( - + { + setNewDelegate(e.target.value) + }} + type="text" + placeholder="Enter Address" + InputProps={{ disableUnderline: true }} + /> ) : null} diff --git a/src/modules/explorer/pages/User/index.tsx b/src/modules/explorer/pages/User/index.tsx index 3eba9b9e..ec243953 100644 --- a/src/modules/explorer/pages/User/index.tsx +++ b/src/modules/explorer/pages/User/index.tsx @@ -109,7 +109,6 @@ export const User: React.FC = () => { const { mutate: unstakeFromAllProposals } = useUnstakeFromAllProposals() const polls = usePolls(data?.liteDAOData?._id) const pollsPosted = polls?.filter(p => p.author === account) - const [voteWeight, setVoteWeight] = useState() useEffect(() => { if (!account) { @@ -168,7 +167,7 @@ export const User: React.FC = () => { - + @@ -208,7 +207,7 @@ export const User: React.FC = () => { - + {proposalsCreated && cycleInfo && ( { const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/operations/delegations?sender=${daoAddress}&status=applied` @@ -17,3 +18,64 @@ export const getLatestDelegation = async (daoAddress: string, network: Network) return resultingDelegations[0] } + +export const getTokenDelegation = async (tokenAddress: string, account: string, network: Network) => { + const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/contracts/${tokenAddress}/bigmaps/delegates/keys?key.eq=${account}&active=true` + const response = await fetch(url) + + if (!response.ok) { + throw new Error("Failed to fetch token delegations from TZKT API") + } + + const resultingDelegations: TokenDelegationDTO[] = await response.json() + + if (resultingDelegations.length === 0) { + return null + } + + const delegatedTo = resultingDelegations[0].value + + return delegatedTo +} + +export const getTokenDelegationVoteWeight = async (tokenAddress: string, account: string, network: Network) => { + const selfBalance = await getUserTokenBalance(account, network, tokenAddress) + + if (!selfBalance) { + throw new Error("Could not fetch delegate token balance from the TZKT API") + } + + const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/contracts/${tokenAddress}/bigmaps/delegates/keys?value.eq=${account}&active=true` + const response = await fetch(url) + + if (!response.ok) { + throw new Error("Failed to fetch token delegations from TZKT API") + } + + const resultingDelegations: TokenDelegationDTO[] = await response.json() + + const delegateBalance: UserDelegateBalance = { + address: account, + balance: selfBalance + } + + if (resultingDelegations.length === 0) { + return [delegateBalance] + } + + const delegatedAddressBalances: UserDelegateBalance[] = [] + + await Promise.all( + resultingDelegations.map(async del => { + const balance = await getUserTokenBalance(del.key, network, tokenAddress) + if (balance) { + delegatedAddressBalances.push({ + address: del.key, + balance: balance + }) + } + }) + ) + + return delegatedAddressBalances +} diff --git a/src/services/bakingBad/delegations/types.ts b/src/services/bakingBad/delegations/types.ts index 21b8ffea..a22e35a3 100644 --- a/src/services/bakingBad/delegations/types.ts +++ b/src/services/bakingBad/delegations/types.ts @@ -23,3 +23,19 @@ export interface DelegationDTO { } status: string } + +export interface TokenDelegationDTO { + id: number + active: boolean + hash: string + key: string + value: string + firstLevel: number + lastLevel: number + updates: number +} + +export interface UserDelegateBalance { + address: string + balance: string +} diff --git a/src/services/contracts/token/hooks/useDelegationStatus.ts b/src/services/contracts/token/hooks/useDelegationStatus.ts new file mode 100644 index 00000000..9158a87a --- /dev/null +++ b/src/services/contracts/token/hooks/useDelegationStatus.ts @@ -0,0 +1,27 @@ +import { useQuery } from "react-query" +import { getTokenDelegation } from "services/bakingBad/delegations" +import { getDAOBalances } from "services/bakingBad/tokenBalances" +import { useTezos } from "services/beacon/hooks/useTezos" + +export const useDelegationStatus = (tokenAddress: string | undefined) => { + const { network, tezos, account, connect } = useTezos() + + const { data, ...rest } = useQuery( + ["tokenDelegations", tokenAddress], + async () => { + if (!tokenAddress) { + return null + } else { + return await getTokenDelegation(tokenAddress, account, network) + } + }, + { + enabled: !!tokenAddress + } + ) + + return { + data, + ...rest + } +} diff --git a/src/services/contracts/token/hooks/useDelegationVoteWeight.ts b/src/services/contracts/token/hooks/useDelegationVoteWeight.ts new file mode 100644 index 00000000..93a26dff --- /dev/null +++ b/src/services/contracts/token/hooks/useDelegationVoteWeight.ts @@ -0,0 +1,25 @@ +import { useQuery } from "react-query" +import { getTokenDelegationVoteWeight } from "services/bakingBad/delegations" +import { UserDelegateBalance } from "services/bakingBad/delegations/types" +import { useTezos } from "services/beacon/hooks/useTezos" + +export const useDelegationVoteWeight = (tokenAddress: string | undefined) => { + const { network, account } = useTezos() + + const { data, ...rest } = useQuery( + ["delegationVoteWeight", tokenAddress], + async () => { + if (tokenAddress) { + return await getTokenDelegationVoteWeight(tokenAddress, account, network) + } + }, + { + enabled: !!tokenAddress + } + ) + + return { + data, + ...rest + } +} diff --git a/src/services/contracts/token/hooks/useTokenDelegate.ts b/src/services/contracts/token/hooks/useTokenDelegate.ts new file mode 100644 index 00000000..415d5d46 --- /dev/null +++ b/src/services/contracts/token/hooks/useTokenDelegate.ts @@ -0,0 +1,63 @@ +import { useNotification } from "modules/common/hooks/useNotification" +import { useMutation, useQueryClient } from "react-query" +import { useTezos } from "services/beacon/hooks/useTezos" +import { networkNameMap } from "../../../bakingBad" +import { setDelegate } from ".." +import { WalletOperation } from "@taquito/taquito" + +export const useTokenDelegate = () => { + const queryClient = useQueryClient() + const openNotification = useNotification() + const { network, tezos, account, connect } = useTezos() + + return useMutation( + async params => { + const { tokenAddress, delegateAddress } = params + // const { key: flushNotification, closeSnackbar: closeFlushNotification } = openNotification({ + // message: "Please sign the transaction to flush", + // persist: true, + // variant: "info" + // }) + try { + let tezosToolkit = tezos + + if (!account) { + tezosToolkit = await connect() + } + + const tx = await setDelegate({ + tokenAddress, + tezos: tezosToolkit, + delegateAddress + }) + // closeFlushNotification(flushNotification) + + if (!tx) { + throw new Error(`Error making delegate transaction`) + } + + openNotification({ + message: "Delegate transaction confirmed!", + autoHideDuration: 5000, + variant: "success", + detailsLink: `https://${networkNameMap[network]}.tzkt.io/` + (tx as WalletOperation).opHash + }) + + return tx + } catch (e: any) { + // closeFlushNotification(flushNotification) + openNotification({ + message: (e as Error).message, + variant: "error", + autoHideDuration: 5000 + }) + return new Error((e as Error).message) + } + }, + { + onSuccess: () => { + queryClient.resetQueries() + } + } + ) +} diff --git a/src/services/contracts/token/index.ts b/src/services/contracts/token/index.ts index b235a152..cd6acfe6 100644 --- a/src/services/contracts/token/index.ts +++ b/src/services/contracts/token/index.ts @@ -3,6 +3,7 @@ import BigNumber from "bignumber.js" import { TokenContractParams } from "modules/creator/deployment/state/types" import { formatUnits } from "../utils" import fa2_single_asset_delegated from "./assets/fa2_single_asset_delegated" +import { getContract } from "../baseDAO" interface Tezos { tezos: TezosToolkit @@ -89,6 +90,24 @@ export const deployTokenContract = async ({ const contract = await c.contract() return contract + } catch (e) { + console.error(e) + return e + } +} + +export const setDelegate = async ({ + tokenAddress, + delegateAddress, + tezos +}: { + tokenAddress: string + delegateAddress: string | null + tezos: TezosToolkit +}) => { + try { + const contract = await getContract(tezos, tokenAddress) + return contract.methods.set_delegate(delegateAddress).send() } catch (e) { console.error(e) }