From 4121183b817ac05b0abce845fe39ebd69896db27 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:12:24 -0400 Subject: [PATCH 01/58] use `streamContractAddress` instead of asset address --- src/components/pages/Roles/types.tsx | 1 + src/hooks/utils/useCreateRoles.ts | 32 +++------------------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/components/pages/Roles/types.tsx b/src/components/pages/Roles/types.tsx index 9ba0f9c9f..e4526c942 100644 --- a/src/components/pages/Roles/types.tsx +++ b/src/components/pages/Roles/types.tsx @@ -160,4 +160,5 @@ export type PreparedEditedStreamData = PreparedNewStreamData & { roleHatId: bigint; roleHatWearer: Address; roleHatSmartAddress: Address; + streamContractAddress: Address; }; diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index c938755a2..e06896601 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -219,6 +219,7 @@ const identifyAndPrepareEditedPaymentStreams = ( roleHatId: BigInt(currentHat.id), roleHatWearer: currentHat.wearer, roleHatSmartAddress: currentHat.smartAddress, + streamContractAddress: payment.contractAddress, }; }); }); @@ -554,7 +555,7 @@ export default function useCreateRoles() { if (payment?.streamId) { const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( payment.streamId, - payment.asset.address, + payment.contractAddress, roleHat.wearer, ); wrappedFlushStreamTxs.push(wrappedFlushStreamTx); @@ -670,36 +671,9 @@ export default function useCreateRoles() { if (editedPaymentStreams.length) { const paymentCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; editedPaymentStreams.forEach(paymentStream => { - const preparedHatFlushAndCancelTxs = prepareHatFlushAndCancelPayment( - paymentStream.streamId, - paymentStream.assetAddress, - paymentStream.roleHatWearer, - ); - paymentCancelTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(paymentStream.roleHatId), paymentStream.roleHatWearer, daoAddress], - }), - targetAddress: hatsProtocol, - }); - paymentCancelTxs.push({ - calldata: preparedHatFlushAndCancelTxs.wrappedFlushStreamTx, - targetAddress: paymentStream.roleHatSmartAddress, - }); - paymentCancelTxs.push(preparedHatFlushAndCancelTxs.cancelStreamTx); - paymentCancelTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(paymentStream.roleHatId), daoAddress, paymentStream.roleHatWearer], - }), - targetAddress: hatsProtocol, - }); - const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( paymentStream.streamId, - paymentStream.assetAddress, + paymentStream.streamContractAddress, paymentStream.roleHatWearer, ); paymentCancelTxs.push({ From 9bab6d80112055cfbefe98f1f9df44b0fb5245cf Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:20:26 -0400 Subject: [PATCH 02/58] use contractAddress --- src/hooks/utils/useCreateRoles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index e06896601..94e1cf3eb 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -617,7 +617,7 @@ export default function useCreateRoles() { if (payment?.streamId) { const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( payment.streamId, - payment.asset.address, + payment.contractAddress, roleHat.wearer, ); hatPaymentWearerChangedTxs.push({ From 8a6ffb3dbc7e0562831c279c5a7d6a05e80634b2 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:35:35 -0400 Subject: [PATCH 03/58] Filter out streams with no withdraw amount from flushing when changing members --- .../pages/Roles/RolePaymentDetails.tsx | 49 ++----- .../Roles/forms/RoleFormCreateProposal.tsx | 3 +- src/components/pages/Roles/types.tsx | 1 + src/hooks/DAO/loaders/useHatsTree.ts | 131 +++++++++++------- src/hooks/utils/useCreateRoles.ts | 74 +++++----- src/store/roles/rolesStoreUtils.ts | 1 + src/store/roles/useRolesStore.ts | 38 +++++ 7 files changed, 179 insertions(+), 118 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index c9dd0ed99..b87a9082e 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -1,17 +1,16 @@ import { Box, Button, Flex, Grid, GridItem, Icon, Image, Text } from '@chakra-ui/react'; import { Calendar, Download } from '@phosphor-icons/react'; import { format } from 'date-fns'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { Address, getAddress, getContract } from 'viem'; -import { useWalletClient, useAccount } from 'wagmi'; -import { SablierV2LockupLinearAbi } from '../../../assets/abi/SablierV2LockupLinear'; +import { Address, getAddress } from 'viem'; +import { useAccount, usePublicClient } from 'wagmi'; import { DETAILS_SHADOW } from '../../../constants/common'; import { DAO_ROUTES } from '../../../constants/routes'; -import { convertStreamIdToBigInt } from '../../../hooks/streams/useCreateSablierStream'; import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useRolesStore } from '../../../store/roles'; import { BigIntValuePair } from '../../../types'; import { DEFAULT_DATE_FORMAT, formatCoin, formatUSD } from '../../../utils'; import { ModalType } from '../../ui/modals/ModalProvider'; @@ -78,6 +77,7 @@ interface RolePaymentDetailsProps { endDate: Date; cliffDate?: Date; isStreaming: () => boolean; + withdrawableAmount?: bigint; }; onClick?: () => void; showWithdraw?: boolean; @@ -95,12 +95,10 @@ export function RolePaymentDetails({ treasury: { assetsFungible }, } = useFractal(); const { address: connectedAccount } = useAccount(); - const { data: walletClient } = useWalletClient(); const { addressPrefix } = useNetworkConfig(); + const { refreshWithdrawableAmount } = useRolesStore(); const navigate = useNavigate(); - - const [withdrawableAmount, setWithdrawableAmount] = useState(0n); - + const publicClient = usePublicClient(); const canWithdraw = useMemo(() => { if (connectedAccount && connectedAccount === roleHatWearerAddress && !!showWithdraw) { return true; @@ -108,33 +106,13 @@ export function RolePaymentDetails({ return false; }, [connectedAccount, showWithdraw, roleHatWearerAddress]); - const loadAmounts = useCallback(async () => { - if (walletClient && payment.streamId && payment.contractAddress && canWithdraw) { - const streamContract = getContract({ - abi: SablierV2LockupLinearAbi, - address: payment.contractAddress, - client: walletClient, - }); - - const bigintStreamId = convertStreamIdToBigInt(payment.streamId); - - const newWithdrawableAmount = await streamContract.read.withdrawableAmountOf([ - bigintStreamId, - ]); - setWithdrawableAmount(newWithdrawableAmount); - } - }, [walletClient, payment, canWithdraw]); - - useEffect(() => { - loadAmounts(); - }, [loadAmounts]); - const [modalType, props] = useMemo(() => { if ( !payment.streamId || !payment.contractAddress || !roleHatWearerAddress || - !roleHatSmartAddress + !roleHatSmartAddress || + !publicClient ) { return [ModalType.NONE] as const; } @@ -146,15 +124,16 @@ export function RolePaymentDetails({ paymentAssetDecimals: payment.asset.decimals, paymentStreamId: payment.streamId, paymentContractAddress: payment.contractAddress, - onSuccess: loadAmounts, + onSuccess: () => + refreshWithdrawableAmount(roleHatSmartAddress, payment.streamId!, publicClient), withdrawInformation: { - withdrawableAmount, + withdrawableAmount: payment.withdrawableAmount, roleHatWearerAddress, roleHatSmartAddress, }, }, ] as const; - }, [payment, roleHatSmartAddress, roleHatWearerAddress, loadAmounts, withdrawableAmount]); + }, [payment, roleHatSmartAddress, roleHatWearerAddress, refreshWithdrawableAmount, publicClient]); const withdraw = useDecentModal(modalType, props); @@ -315,7 +294,7 @@ export function RolePaymentDetails({ /> - {canWithdraw && withdrawableAmount > 0n && ( + {canWithdraw && !!payment?.withdrawableAmount && payment.withdrawableAmount > 0n && ( void }) amount: payment.amount, asset: payment.asset, cliffDate: payment.cliffDate, + withdrawableAmount: 0n, }; }) : [], - }; + }; }); }, [values.hats]); diff --git a/src/components/pages/Roles/types.tsx b/src/components/pages/Roles/types.tsx index e4526c942..e09c78e95 100644 --- a/src/components/pages/Roles/types.tsx +++ b/src/components/pages/Roles/types.tsx @@ -22,6 +22,7 @@ export interface SablierPayment extends BaseSablierStream { endDate: Date; cliffDate: Date | undefined; isStreaming: () => boolean; + withdrawableAmount: bigint; } export interface SablierPaymentFormValues extends Partial { diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 22a24250e..02c8e31d5 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -2,13 +2,15 @@ import { useApolloClient } from '@apollo/client'; import { HatsSubgraphClient, Tree } from '@hatsprotocol/sdk-v1-subgraph'; import { useEffect } from 'react'; import { toast } from 'react-toastify'; -import { formatUnits, getAddress } from 'viem'; +import { formatUnits, getAddress, getContract } from 'viem'; import { usePublicClient } from 'wagmi'; import { StreamsQueryDocument } from '../../../../.graphclient'; +import { SablierV2LockupLinearAbi } from '../../../assets/abi/SablierV2LockupLinear'; import { SablierPayment } from '../../../components/pages/Roles/types'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { DecentHatsError, useRolesStore } from '../../../store/roles'; +import { convertStreamIdToBigInt } from '../../streams/useCreateSablierStream'; import { CacheExpiry, CacheKeys } from '../../utils/cache/cacheDefaults'; import { getValue, setValue } from '../../utils/cache/useLocalStorage'; @@ -154,7 +156,13 @@ const useHatsTree = () => { useEffect(() => { async function getHatsStreams() { - if (sablierSubgraph && hatsTree && hatsTree.roleHats.length > 0 && !streamsFetched) { + if ( + sablierSubgraph && + hatsTree && + hatsTree.roleHats.length > 0 && + !streamsFetched && + publicClient + ) { const secondsTimestampToDate = (ts: string) => new Date(Number(ts) * 1000); const updatedHatsRoles = await Promise.all( hatsTree.roleHats.map(async hat => { @@ -176,56 +184,70 @@ const useHatsTree = () => { const lockupLinearStreams = streamQueryResult.data.streams.filter( stream => stream.category === 'LockupLinear', ); - const formattedActiveStreams: SablierPayment[] = lockupLinearStreams.map( - lockupLinearStream => { - const parsedAmount = formatUnits( - BigInt(lockupLinearStream.depositAmount), - lockupLinearStream.asset.decimals, - ); - - const startDate = secondsTimestampToDate(lockupLinearStream.startTime); - const endDate = secondsTimestampToDate(lockupLinearStream.endTime); - const cliffDate = lockupLinearStream.cliff - ? secondsTimestampToDate(lockupLinearStream.cliffTime) - : undefined; - - return { - streamId: lockupLinearStream.id, - contractAddress: lockupLinearStream.contract.address, - asset: { - address: getAddress( - lockupLinearStream.asset.address, - lockupLinearStream.asset.chainId, - ), - name: lockupLinearStream.asset.name, - symbol: lockupLinearStream.asset.symbol, - decimals: lockupLinearStream.asset.decimals, - logo: '', // @todo - how do we get logo? - }, - amount: { - bigintValue: BigInt(lockupLinearStream.depositAmount), - value: parsedAmount, - }, - startDate, - endDate, - cliffDate, - isStreaming: () => { - const start = !lockupLinearStream.cliff - ? startDate.getTime() - : cliffDate !== undefined - ? cliffDate.getTime() - : undefined; - const end = endDate ? endDate.getTime() : undefined; - const cancelled = lockupLinearStream.canceled; - const now = new Date().getTime(); - - return !cancelled && !!start && !!end && start <= now && end > now; - }, - }; - }, + const formattedLinearStreams = lockupLinearStreams.map(lockupLinearStream => { + const parsedAmount = formatUnits( + BigInt(lockupLinearStream.depositAmount), + lockupLinearStream.asset.decimals, + ); + + const startDate = secondsTimestampToDate(lockupLinearStream.startTime); + const endDate = secondsTimestampToDate(lockupLinearStream.endTime); + const cliffDate = lockupLinearStream.cliff + ? secondsTimestampToDate(lockupLinearStream.cliffTime) + : undefined; + + return { + streamId: lockupLinearStream.id, + contractAddress: lockupLinearStream.contract.address, + asset: { + address: getAddress( + lockupLinearStream.asset.address, + lockupLinearStream.asset.chainId, + ), + name: lockupLinearStream.asset.name, + symbol: lockupLinearStream.asset.symbol, + decimals: lockupLinearStream.asset.decimals, + logo: '', // @todo - how do we get logo? + }, + amount: { + bigintValue: BigInt(lockupLinearStream.depositAmount), + value: parsedAmount, + }, + startDate, + endDate, + cliffDate, + isStreaming: () => { + const start = !lockupLinearStream.cliff + ? startDate.getTime() + : cliffDate !== undefined + ? cliffDate.getTime() + : undefined; + const end = endDate ? endDate.getTime() : undefined; + const cancelled = lockupLinearStream.canceled; + const now = new Date().getTime(); + + return !cancelled && !!start && !!end && start <= now && end > now; + }, + }; + }); + + const streamsWithCurrentWithdrawableAmounts: SablierPayment[] = await Promise.all( + formattedLinearStreams.map(async stream => { + const streamContract = getContract({ + abi: SablierV2LockupLinearAbi, + address: stream.contractAddress, + client: publicClient, + }); + const bigintStreamId = convertStreamIdToBigInt(stream.streamId); + + const newWithdrawableAmount = await streamContract.read.withdrawableAmountOf([ + bigintStreamId, + ]); + return { ...stream, withdrawableAmount: newWithdrawableAmount }; + }), ); - return { ...hat, payments: formattedActiveStreams }; + return { ...hat, payments: streamsWithCurrentWithdrawableAmounts }; } else { return hat; } @@ -237,7 +259,14 @@ const useHatsTree = () => { } getHatsStreams(); - }, [apolloClient, hatsTree, sablierSubgraph, updateRolesWithStreams, streamsFetched]); + }, [ + apolloClient, + hatsTree, + sablierSubgraph, + updateRolesWithStreams, + streamsFetched, + publicClient, + ]); }; export { useHatsTree }; diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 94e1cf3eb..7bd3b682b 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { Address, encodeFunctionData, getAddress, Hash, Hex, zeroAddress } from 'viem'; +import { Address, encodeFunctionData, getAddress, Hex, zeroAddress } from 'viem'; import { usePublicClient } from 'wagmi'; import DecentHatsAbi from '../../assets/abi/DecentHats_0_1_0_Abi'; import ERC6551RegistryAbi from '../../assets/abi/ERC6551RegistryAbi'; @@ -609,47 +609,56 @@ export default function useCreateRoles() { } if (memberChangedHats.length) { - transferHatTxs = memberChangedHats - .map(({ id, currentWearer, newWearer }) => { - const roleHat = hatsTree.roleHats.find(hat => hat.id === id); - if (roleHat && roleHat.payments?.length) { - const payment = roleHat.payments[0]; - if (payment?.streamId) { + memberChangedHats.map(({ id, currentWearer, newWearer }) => { + const roleHat = hatsTree.roleHats.find(hat => hat.id === id); + if (roleHat && roleHat.payments?.length) { + /** + * Assumption: current state of blockchain + * Fact: does the hat currently have funds to withdraw + * Do: if yes, then flush the stream + */ + const fundsToClaimStreams = roleHat.payments.filter( + payment => payment.withdrawableAmount > 0n, + ); + if (fundsToClaimStreams.length) { + hatPaymentWearerChangedTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(id), currentWearer, daoAddress], + }), + targetAddress: hatsProtocol, + }); + fundsToClaimStreams.forEach(payment => { const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( payment.streamId, payment.contractAddress, roleHat.wearer, ); - hatPaymentWearerChangedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), currentWearer, daoAddress], - }), - targetAddress: hatsProtocol, - }); hatPaymentWearerChangedTxs.push({ calldata: wrappedFlushStreamTx, targetAddress: roleHat.smartAddress, }); - hatPaymentWearerChangedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), daoAddress, newWearer], - }), - targetAddress: hatsProtocol, - }); - } - } else { - return encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), currentWearer, newWearer], + }); + hatPaymentWearerChangedTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(id), daoAddress, newWearer], + }), + targetAddress: hatsProtocol, }); } - }) - .filter(data => !!data) as Hash[]; + } else { + transferHatTxs.push( + encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(id), daoAddress, newWearer], + }), + ); + } + }); } if (roleDetailsChangedHats.length) { @@ -710,6 +719,7 @@ export default function useCreateRoles() { ...createAndMintHatsTxs.map(() => hatsProtocol), ...smartAccountTxs.map(({ targetAddress }) => targetAddress), ...removeHatTxs.map(() => topHatAccount), + ...hatPaymentWearerChangedTxs.map(({ targetAddress }) => targetAddress), ...transferHatTxs.map(() => hatsProtocol), ...hatDetailsChangedTxs.map(() => hatsProtocol), ...hatPaymentAddedTxs.map(({ targetAddress }) => targetAddress), @@ -719,6 +729,7 @@ export default function useCreateRoles() { ...createAndMintHatsTxs, ...smartAccountTxs.map(({ calldata }) => calldata), ...removeHatTxs, + ...hatPaymentWearerChangedTxs.map(({ calldata }) => calldata), ...transferHatTxs, ...hatDetailsChangedTxs, ...hatPaymentAddedTxs.map(({ calldata }) => calldata), @@ -729,6 +740,7 @@ export default function useCreateRoles() { ...createAndMintHatsTxs.map(() => 0n), ...smartAccountTxs.map(() => 0n), ...removeHatTxs.map(() => 0n), + ...hatPaymentWearerChangedTxs.map(() => 0n), ...transferHatTxs.map(() => 0n), ...hatDetailsChangedTxs.map(() => 0n), ...hatPaymentAddedTxs.map(() => 0n), diff --git a/src/store/roles/rolesStoreUtils.ts b/src/store/roles/rolesStoreUtils.ts index 4c0fec110..fb44255a1 100644 --- a/src/store/roles/rolesStoreUtils.ts +++ b/src/store/roles/rolesStoreUtils.ts @@ -68,6 +68,7 @@ export interface RolesStore extends RolesStoreData { publicClient: PublicClient; decentHats: Address; }) => Promise; + refreshWithdrawableAmount: (hatId: Hex, streamId: string, publicClient: PublicClient) => void; updateRolesWithStreams: (updatedRolesWithStreams: DecentRoleHat[]) => void; resetHatsStore: () => void; } diff --git a/src/store/roles/useRolesStore.ts b/src/store/roles/useRolesStore.ts index 0ea2ee2fe..45afd4c27 100644 --- a/src/store/roles/useRolesStore.ts +++ b/src/store/roles/useRolesStore.ts @@ -1,4 +1,7 @@ +import { getContract, Hex, PublicClient } from 'viem'; import { create } from 'zustand'; +import { SablierV2LockupLinearAbi } from '../../assets/abi/SablierV2LockupLinear'; +import { convertStreamIdToBigInt } from '../../hooks/streams/useCreateSablierStream'; import { DecentRoleHat, initialHatsStore, RolesStore, sanitize } from './rolesStoreUtils'; const useRolesStore = create()((set, get) => ({ @@ -57,6 +60,41 @@ const useRolesStore = create()((set, get) => ({ ); set(() => ({ hatsTree })); }, + refreshWithdrawableAmount: async ( + hatId: Hex, + streamId: string, + publicClient: PublicClient, + ) => { + const payment = get().getPayment(hatId, streamId); + if (!payment) return; + + const streamContract = getContract({ + abi: SablierV2LockupLinearAbi, + address: payment.contractAddress, + client: publicClient, + }); + + const bigintStreamId = convertStreamIdToBigInt(streamId); + + const newWithdrawableAmount = await streamContract.read.withdrawableAmountOf([bigintStreamId]); + const currentHatsTree = get().hatsTree; + + if (!currentHatsTree) return; + set(() => ({ + hatsTree: { + ...currentHatsTree, + roleHats: currentHatsTree.roleHats.map(roleHat => { + if (roleHat.id !== hatId) return roleHat; + return { + ...roleHat, + payments: roleHat.payments?.map(p => + p.streamId === streamId ? { ...p, withdrawableAmount: newWithdrawableAmount } : p, + ), + }; + }), + }, + })); + }, updateRolesWithStreams: (updatedRoles: DecentRoleHat[]) => { const existingHatsTree = get().hatsTree; if (!existingHatsTree) return; From 0fe07257f29b17cd9f8740362fdc888ec56a4a77 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:15:21 -0400 Subject: [PATCH 04/58] Remove Role tested workflow and updates --- .../Roles/forms/RoleFormCreateProposal.tsx | 1 + src/components/pages/Roles/types.tsx | 1 + src/hooks/DAO/loaders/useHatsTree.ts | 1 + src/hooks/utils/useCreateRoles.ts | 86 +++++++++++-------- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx b/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx index 039913b70..a2191f696 100644 --- a/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx +++ b/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx @@ -55,6 +55,7 @@ export default function RoleFormCreateProposal({ close }: { close: () => void }) asset: payment.asset, cliffDate: payment.cliffDate, withdrawableAmount: 0n, + isCancelled: false, }; }) : [], diff --git a/src/components/pages/Roles/types.tsx b/src/components/pages/Roles/types.tsx index e09c78e95..7c2eeb828 100644 --- a/src/components/pages/Roles/types.tsx +++ b/src/components/pages/Roles/types.tsx @@ -23,6 +23,7 @@ export interface SablierPayment extends BaseSablierStream { cliffDate: Date | undefined; isStreaming: () => boolean; withdrawableAmount: bigint; + isCancelled: boolean; } export interface SablierPaymentFormValues extends Partial { diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 02c8e31d5..30f3ad462 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -213,6 +213,7 @@ const useHatsTree = () => { bigintValue: BigInt(lockupLinearStream.depositAmount), value: parsedAmount, }, + isCancelled: lockupLinearStream.canceled, startDate, endDate, cliffDate, diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 7bd3b682b..8a3992976 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -545,23 +545,19 @@ export default function useCreateRoles() { } if (removedHatIds.length) { - removeHatTxs = removedHatIds.map(hatId => { + removedHatIds.forEach(hatId => { const roleHat = hatsTree.roleHats.find(hat => hat.id === hatId); - if (roleHat) { - if (roleHat.payments?.length) { - const wrappedFlushStreamTxs: Hex[] = []; - const cancelStreamTxs: { calldata: Hex; targetAddress: Address }[] = []; - roleHat.payments.forEach(payment => { - if (payment?.streamId) { - const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( - payment.streamId, - payment.contractAddress, - roleHat.wearer, - ); - wrappedFlushStreamTxs.push(wrappedFlushStreamTx); - cancelStreamTxs.push(cancelStreamTx); - } - }); + if (roleHat && roleHat.payments?.length) { + /** + * Assumption: current state of blockchain + * Fact: does the hat currently have funds to withdraw + * Do: if yes, then flush the stream + */ + + const fundsToClaimStreams = roleHat.payments.filter( + payment => payment.withdrawableAmount > 0n, + ); + if (fundsToClaimStreams.length) { hatPaymentHatRemovedTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -570,15 +566,32 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - wrappedFlushStreamTxs.forEach(wrappedFlushStreamTx => { + fundsToClaimStreams.forEach(payment => { + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + payment.streamId, + payment.contractAddress, + roleHat.wearer, + ); hatPaymentHatRemovedTxs.push({ calldata: wrappedFlushStreamTx, targetAddress: roleHat.smartAddress, }); }); - cancelStreamTxs.forEach(cancelStreamTx => { - hatPaymentHatRemovedTxs.push(cancelStreamTx); - }); + } + /** + * Assumption: current state of blockchain + * Fact: does the hat currently have funds that are not active or have not ended + * Do: if yes, then cancel the stream + */ + const streamsToCancel = roleHat.payments.filter( + payment => !payment.isCancelled && payment.endDate > new Date(), + ); + streamsToCancel.forEach(payment => { + hatPaymentHatRemovedTxs.push( + prepareCancelStreamTx(payment.streamId, payment.contractAddress), + ); + }); + if (streamsToCancel.length || fundsToClaimStreams.length) { hatPaymentHatRemovedTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -591,20 +604,22 @@ export default function useCreateRoles() { } // make transaction proxy through erc6551 contract - return encodeFunctionData({ - abi: HatsAccount1ofNAbi, - functionName: 'execute', - args: [ - hatsProtocol, - 0n, - encodeFunctionData({ - abi: HatsAbi, - functionName: 'setHatStatus', - args: [BigInt(hatId), false], - }), - 0, - ], - }); + removeHatTxs.push( + encodeFunctionData({ + abi: HatsAccount1ofNAbi, + functionName: 'execute', + args: [ + hatsProtocol, + 0n, + encodeFunctionData({ + abi: HatsAbi, + functionName: 'setHatStatus', + args: [BigInt(hatId), false], + }), + 0, + ], + }), + ); }); } @@ -718,6 +733,7 @@ export default function useCreateRoles() { targets: [ ...createAndMintHatsTxs.map(() => hatsProtocol), ...smartAccountTxs.map(({ targetAddress }) => targetAddress), + ...hatPaymentHatRemovedTxs.map(({ targetAddress }) => targetAddress), ...removeHatTxs.map(() => topHatAccount), ...hatPaymentWearerChangedTxs.map(({ targetAddress }) => targetAddress), ...transferHatTxs.map(() => hatsProtocol), @@ -728,6 +744,7 @@ export default function useCreateRoles() { calldatas: [ ...createAndMintHatsTxs, ...smartAccountTxs.map(({ calldata }) => calldata), + ...hatPaymentHatRemovedTxs.map(({ calldata }) => calldata), ...removeHatTxs, ...hatPaymentWearerChangedTxs.map(({ calldata }) => calldata), ...transferHatTxs, @@ -739,6 +756,7 @@ export default function useCreateRoles() { values: [ ...createAndMintHatsTxs.map(() => 0n), ...smartAccountTxs.map(() => 0n), + ...hatPaymentHatRemovedTxs.map(() => 0n), ...removeHatTxs.map(() => 0n), ...hatPaymentWearerChangedTxs.map(() => 0n), ...transferHatTxs.map(() => 0n), From 18405740882fa10d52cb9b363b5fcb0dfcdb6cf9 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:31:51 -0400 Subject: [PATCH 05/58] editPayment updates --- src/hooks/utils/useCreateRoles.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 8a3992976..69bb48f5d 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -693,6 +693,11 @@ export default function useCreateRoles() { } if (editedPaymentStreams.length) { + /** + * Assumption: current state of blockchain + * Fact: does the hat currently have funds to withdraw + * Do: if yes, then flush the stream + */ const paymentCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; editedPaymentStreams.forEach(paymentStream => { const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( @@ -708,6 +713,11 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); + const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( + paymentStream.streamId, + paymentStream.streamContractAddress, + paymentStream.roleHatWearer, + ); paymentCancelTxs.push({ calldata: wrappedFlushStreamTx, targetAddress: paymentStream.roleHatSmartAddress, @@ -771,6 +781,7 @@ export default function useCreateRoles() { [ hatsProtocol, hatsTree, + prepareCancelStreamTx, prepareHatFlushAndCancelPayment, prepareHatsAccountFlushExecData, daoAddress, From b589a51e1d5d934651207185240f2c91f9cf5773 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Tue, 3 Sep 2024 14:30:29 -0400 Subject: [PATCH 06/58] pretty --- src/components/pages/Roles/forms/RoleFormCreateProposal.tsx | 2 +- src/store/roles/useRolesStore.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx b/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx index a2191f696..7a85db376 100644 --- a/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx +++ b/src/components/pages/Roles/forms/RoleFormCreateProposal.tsx @@ -59,7 +59,7 @@ export default function RoleFormCreateProposal({ close }: { close: () => void }) }; }) : [], - }; + }; }); }, [values.hats]); diff --git a/src/store/roles/useRolesStore.ts b/src/store/roles/useRolesStore.ts index 45afd4c27..4a4dc8c13 100644 --- a/src/store/roles/useRolesStore.ts +++ b/src/store/roles/useRolesStore.ts @@ -60,11 +60,7 @@ const useRolesStore = create()((set, get) => ({ ); set(() => ({ hatsTree })); }, - refreshWithdrawableAmount: async ( - hatId: Hex, - streamId: string, - publicClient: PublicClient, - ) => { + refreshWithdrawableAmount: async (hatId: Hex, streamId: string, publicClient: PublicClient) => { const payment = get().getPayment(hatId, streamId); if (!payment) return; From 8e05298e2eabdc66bf388b11b6cb6b79c5dade7d Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Tue, 3 Sep 2024 14:33:41 -0400 Subject: [PATCH 07/58] Remove redundant code --- src/hooks/utils/useCreateRoles.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 69bb48f5d..aca6a5c83 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -713,11 +713,6 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( - paymentStream.streamId, - paymentStream.streamContractAddress, - paymentStream.roleHatWearer, - ); paymentCancelTxs.push({ calldata: wrappedFlushStreamTx, targetAddress: paymentStream.roleHatSmartAddress, From 84e4abffadf43c1d32792259eff657ec3d3e26e9 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Fri, 30 Aug 2024 18:10:39 -0400 Subject: [PATCH 08/58] Rewrite wip --- src/hooks/streams/useCreateSablierStream.ts | 1 + src/hooks/utils/useCreateRoles.ts | 1446 +++++++++++-------- 2 files changed, 816 insertions(+), 631 deletions(-) diff --git a/src/hooks/streams/useCreateSablierStream.ts b/src/hooks/streams/useCreateSablierStream.ts index 0af300e02..7c68e20e8 100644 --- a/src/hooks/streams/useCreateSablierStream.ts +++ b/src/hooks/streams/useCreateSablierStream.ts @@ -144,5 +144,6 @@ export default function useCreateSablierStream() { prepareBatchLinearStreamCreation, prepareFlushStreamTx, prepareCancelStreamTx, + prepareLinearStream, }; } diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index aca6a5c83..dfbf5f090 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -42,269 +42,281 @@ const hatsDetailsBuilder = (data: { name: string; description: string }) => { }); }; -const identifyAndPrepareAddedHats = async ( - modifiedHats: RoleHatFormValueEdited[], +const createHatStruct = async ( + name: string, + description: string, + wearer: Address, uploadHatDescription: (hatDescription: string) => Promise, ) => { - let roleHatFormValuesPlus: (RoleHatFormValueEdited & { wearer: Address; details: string })[] = []; - - const hatStructs = await Promise.all( - modifiedHats - .filter(formHat => formHat.editedRole.status === EditBadgeStatus.New) - .map(async formHat => { - if (formHat.name === undefined || formHat.description === undefined) { - throw new Error('Hat name or description of added hat is undefined.'); - } - - if (formHat.wearer === undefined) { - throw new Error('Hat wearer of added hat is undefined.'); - } - - const details = await uploadHatDescription( - hatsDetailsBuilder({ - name: formHat.name, - description: formHat.description, - }), - ); - - const newHat: HatStruct = { - maxSupply: 1, - details, - imageURI: '', - isMutable: true, - wearer: getAddress(formHat.wearer), - }; - - roleHatFormValuesPlus.push({ ...formHat, wearer: getAddress(formHat.wearer), details }); - return newHat; - }), + const details = await uploadHatDescription( + hatsDetailsBuilder({ + name: name, + description: description, + }), ); - return [hatStructs, roleHatFormValuesPlus] as const; -}; - -const identifyAndPrepareRemovedHats = (modifiedHats: RoleHatFormValueEdited[]) => { - return modifiedHats - .filter(hat => hat.editedRole.status === EditBadgeStatus.Removed) - .map(hat => { - if (hat.id === undefined) { - throw new Error('Hat ID of removed hat is undefined.'); - } + const newHat: HatStruct = { + maxSupply: 1, + details, + imageURI: '', + isMutable: true, + wearer: wearer, + }; - return hat.id; - }); + return newHat; }; -const identifyAndPrepareMemberChangedHats = ( - modifiedHats: RoleHatFormValueEdited[], - getHat: (hatId: Hex) => DecentRoleHat | null, +const createHatStructFromRoleFormValues = async ( + role: RoleHatFormValueEdited, + uploadHatDescription: (hatDescription: string) => Promise, ) => { - return modifiedHats - .filter( - hat => - hat.editedRole.status === EditBadgeStatus.Updated && - hat.editedRole.fieldNames.includes('member'), - ) - .map(hat => { - if (hat.wearer === undefined || hat.id === undefined) { - throw new Error('Hat wearer of member changed Hat is undefined.'); - } - - const currentHat = getHat(hat.id); - if (currentHat === null) { - throw new Error("Couldn't find existing Hat for member changed Hat."); - } - - const hatWearerChanged = { - id: currentHat.id, - currentWearer: getAddress(currentHat.wearer), - newWearer: getAddress(hat.wearer), - }; - - return hatWearerChanged; - }) - .filter(hat => hat.currentWearer !== hat.newWearer); + if (role.name === undefined || role.description === undefined) { + throw new Error('Hat name or description of added hat is undefined.'); + } + + if (role.wearer === undefined) { + throw new Error('Hat wearer of added hat is undefined.'); + } + + return createHatStruct( + role.name, + role.description, + getAddress(role.wearer), + uploadHatDescription, + ); }; -const identifyAndPrepareRoleDetailsChangedHats = ( - modifiedHats: RoleHatFormValueEdited[], +const createHatStructsFromRolesFormValues = async ( + modifiedRoles: RoleHatFormValueEdited[], uploadHatDescription: (hatDescription: string) => Promise, - getHat: (hatId: Hex) => DecentRoleHat | null, ) => { return Promise.all( - modifiedHats - .filter( - formHat => - formHat.editedRole.status === EditBadgeStatus.Updated && - (formHat.editedRole.fieldNames.includes('roleName') || - formHat.editedRole.fieldNames.includes('roleDescription')), - ) - .map(async formHat => { - if (formHat.id === undefined) { - throw new Error('Hat ID of existing hat is undefined.'); - } - - if (formHat.name === undefined || formHat.description === undefined) { - throw new Error('Hat name or description of existing hat is undefined.'); - } - - const currentHat = getHat(formHat.id); - if (currentHat === null) { - throw new Error("Couldn't find existing Hat for details changed Hat."); - } - - return { - id: currentHat.id, - details: await uploadHatDescription( - hatsDetailsBuilder({ - name: formHat.name, - description: formHat.description, - }), - ), - }; - }), + modifiedRoles.map(role => createHatStructFromRoleFormValues(role, uploadHatDescription)), ); }; -const identifyAndPrepareEditedPaymentStreams = ( - modifiedHats: RoleHatFormValueEdited[], - getHat: (hatId: Hex) => DecentRoleHat | null, - getPayment: (hatId: Hex, streamId: string) => SablierPayment | null, -): PreparedEditedStreamData[] => { - return modifiedHats.flatMap(formHat => { - const currentHat = getHat(formHat.id); - if (currentHat === null) { - return []; - } - - if (formHat.payments === undefined) { - return []; - } - - return formHat.payments - .filter(payment => { - if (payment.streamId === undefined) { - return false; - } - // @note remove payments that haven't been edited - const originalPayment = getPayment(formHat.id, payment.streamId); - if (originalPayment === null) { - return false; - } - return !isEqual(payment, originalPayment); - }) - .map(payment => { - if ( - !payment.streamId || - !payment.contractAddress || - !payment.asset || - !payment.startDate || - !payment.endDate || - !payment.amount?.bigintValue || - payment.amount.bigintValue <= 0n - ) { - throw new Error('Form Values inValid', { - cause: payment, - }); - } - - return { - streamId: payment.streamId, - recipient: currentHat.smartAddress, - startDateTs: Math.floor(payment.startDate.getTime() / 1000), - endDateTs: Math.ceil(payment.endDate.getTime() / 1000), - cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: payment.amount.bigintValue, - assetAddress: payment.asset.address, - roleHatId: BigInt(currentHat.id), - roleHatWearer: currentHat.wearer, - roleHatSmartAddress: currentHat.smartAddress, - streamContractAddress: payment.contractAddress, - }; - }); - }); -}; - -const identifyAndPrepareAddedPaymentStreams = async ( - modifiedHats: RoleHatFormValueEdited[], - addedHatsWithIds: AddedHatsWithIds[], - getHat: (hatId: Hex) => DecentRoleHat | null, - predictSmartAccount: (hatId: bigint) => Promise
, -): Promise => { - const preparedStreamDataMapped = await Promise.all( - modifiedHats.map(async formHat => { - if (formHat.payments === undefined) { - return []; - } - - const payments = formHat.payments.filter(payment => !payment.streamId); - let recipientAddress: Address; - const existingRoleHat = getHat(formHat.id); - if (!!existingRoleHat) { - recipientAddress = existingRoleHat.smartAddress; - } else { - const addedRoleHat = addedHatsWithIds.find(addedHat => addedHat.formId === formHat.id); - if (!addedRoleHat) { - throw new Error('Could not find added role hat for added payment stream.'); - } - recipientAddress = await predictSmartAccount(addedRoleHat.id); - } - return payments.map(payment => { - if ( - !payment.asset || - !payment.startDate || - !payment.endDate || - !payment.amount?.bigintValue || - payment.amount.bigintValue <= 0n - ) { - throw new Error('Form Values inValid', { - cause: payment, - }); - } - - return { - recipient: recipientAddress, - startDateTs: Math.floor(payment.startDate.getTime() / 1000), - endDateTs: Math.ceil(payment.endDate.getTime() / 1000), - cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: payment.amount.bigintValue, - assetAddress: payment.asset.address, - }; - }); - }), - ); - return preparedStreamDataMapped.flat(); -}; - -const prepareAddHatsTxArgs = (addedHats: HatStruct[], adminHatId: Hex, topHatAccount: Address) => { - const admins: bigint[] = []; - const details: string[] = []; - const maxSupplies: number[] = []; - const eligibilityModules: Address[] = []; - const toggleModules: Address[] = []; - const mutables: boolean[] = []; - const imageURIs: string[] = []; - - addedHats.forEach(hat => { - admins.push(BigInt(adminHatId)); - details.push(hat.details); - maxSupplies.push(hat.maxSupply); - eligibilityModules.push(topHatAccount); - toggleModules.push(topHatAccount); - mutables.push(hat.isMutable); - imageURIs.push(hat.imageURI); - }); - - return [ - admins, - details, - maxSupplies, - eligibilityModules, - toggleModules, - mutables, - imageURIs, - ] as const; -}; +// const identifyAndPrepareRemovedHats = (modifiedHats: RoleHatFormValueEdited[]) => { +// return modifiedHats +// .filter(hat => hat.editedRole.status === EditBadgeStatus.Removed) +// .map(hat => { +// if (hat.id === undefined) { +// throw new Error('Hat ID of removed hat is undefined.'); +// } + +// return hat.id; +// }); +// }; + +// const identifyAndPrepareMemberChangedHats = ( +// modifiedHats: RoleHatFormValueEdited[], +// getHat: (hatId: Hex) => DecentRoleHat | null, +// ) => { +// return modifiedHats +// .filter( +// hat => +// hat.editedRole.status === EditBadgeStatus.Updated && +// hat.editedRole.fieldNames.includes('member'), +// ) +// .map(hat => { +// if (hat.wearer === undefined || hat.id === undefined) { +// throw new Error('Hat wearer of member changed Hat is undefined.'); +// } + +// const currentHat = getHat(hat.id); +// if (currentHat === null) { +// throw new Error("Couldn't find existing Hat for member changed Hat."); +// } + +// const hatWearerChanged = { +// id: currentHat.id, +// currentWearer: getAddress(currentHat.wearer), +// newWearer: getAddress(hat.wearer), +// }; + +// return hatWearerChanged; +// }) +// .filter(hat => hat.currentWearer !== hat.newWearer); +// }; + +// const identifyAndPrepareRoleDetailsChangedHats = ( +// modifiedHats: RoleHatFormValueEdited[], +// uploadHatDescription: (hatDescription: string) => Promise, +// getHat: (hatId: Hex) => DecentRoleHat | null, +// ) => { +// return Promise.all( +// modifiedHats +// .filter( +// formHat => +// formHat.editedRole.status === EditBadgeStatus.Updated && +// (formHat.editedRole.fieldNames.includes('roleName') || +// formHat.editedRole.fieldNames.includes('roleDescription')), +// ) +// .map(async formHat => { +// if (formHat.id === undefined) { +// throw new Error('Hat ID of existing hat is undefined.'); +// } + +// if (formHat.name === undefined || formHat.description === undefined) { +// throw new Error('Hat name or description of existing hat is undefined.'); +// } + +// const currentHat = getHat(formHat.id); +// if (currentHat === null) { +// throw new Error("Couldn't find existing Hat for details changed Hat."); +// } + +// return { +// id: currentHat.id, +// details: await uploadHatDescription( +// hatsDetailsBuilder({ +// name: formHat.name, +// description: formHat.description, +// }), +// ), +// }; +// }), +// ); +// }; + +// const identifyAndPrepareEditedPaymentStreams = ( +// modifiedHats: RoleHatFormValueEdited[], +// getHat: (hatId: Hex) => DecentRoleHat | null, +// getPayment: (hatId: Hex, streamId: string) => SablierPayment | null, +// ): PreparedEditedStreamData[] => { +// return modifiedHats.flatMap(formHat => { +// const currentHat = getHat(formHat.id); +// if (currentHat === null) { +// return []; +// } + +// if (formHat.payments === undefined) { +// return []; +// } + +// return formHat.payments +// .filter(payment => { +// if (payment.streamId === undefined) { +// return false; +// } +// // @note remove payments that haven't been edited +// const originalPayment = getPayment(formHat.id, payment.streamId); +// if (originalPayment === null) { +// return false; +// } +// return !isEqual(payment, originalPayment); +// }) +// .map(payment => { +// if ( +// !payment.streamId || +// !payment.contractAddress || +// !payment.asset || +// !payment.startDate || +// !payment.endDate || +// !payment.amount?.bigintValue || +// payment.amount.bigintValue <= 0n +// ) { +// throw new Error('Form Values inValid', { +// cause: payment, +// }); +// } + +// return { +// streamId: payment.streamId, +// recipient: currentHat.smartAddress, +// startDateTs: Math.floor(payment.startDate.getTime() / 1000), +// endDateTs: Math.ceil(payment.endDate.getTime() / 1000), +// cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), +// totalAmount: payment.amount.bigintValue, +// assetAddress: payment.asset.address, +// roleHatId: BigInt(currentHat.id), +// roleHatWearer: currentHat.wearer, +// roleHatSmartAddress: currentHat.smartAddress, +// streamContractAddress: payment.contractAddress, +// }; +// }); +// }); +// }; + +// const identifyAndPrepareAddedPaymentStreams = async ( +// modifiedHats: RoleHatFormValueEdited[], +// addedHatsWithIds: AddedHatsWithIds[], +// getHat: (hatId: Hex) => DecentRoleHat | null, +// predictSmartAccount: (hatId: bigint) => Promise
, +// ): Promise => { +// const preparedStreamDataMapped = await Promise.all( +// modifiedHats.map(async formHat => { +// if (formHat.payments === undefined) { +// return []; +// } + +// const payments = formHat.payments.filter(payment => !payment.streamId); +// let recipientAddress: Address; +// const existingRoleHat = getHat(formHat.id); +// if (!!existingRoleHat) { +// recipientAddress = existingRoleHat.smartAddress; +// } else { +// const addedRoleHat = addedHatsWithIds.find(addedHat => addedHat.formId === formHat.id); +// if (!addedRoleHat) { +// throw new Error('Could not find added role hat for added payment stream.'); +// } +// recipientAddress = await predictSmartAccount(addedRoleHat.id); +// } +// return payments.map(payment => { +// if ( +// !payment.asset || +// !payment.startDate || +// !payment.endDate || +// !payment.amount?.bigintValue || +// payment.amount.bigintValue <= 0n +// ) { +// throw new Error('Form Values inValid', { +// cause: payment, +// }); +// } + +// return { +// recipient: recipientAddress, +// startDateTs: Math.floor(payment.startDate.getTime() / 1000), +// endDateTs: Math.ceil(payment.endDate.getTime() / 1000), +// cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), +// totalAmount: payment.amount.bigintValue, +// assetAddress: payment.asset.address, +// }; +// }); +// }), +// ); +// return preparedStreamDataMapped.flat(); +// }; + +// const prepareAddHatsTxArgs = (addedHats: HatStruct[], adminHatId: Hex, topHatAccount: Address) => { +// const admins: bigint[] = []; +// const details: string[] = []; +// const maxSupplies: number[] = []; +// const eligibilityModules: Address[] = []; +// const toggleModules: Address[] = []; +// const mutables: boolean[] = []; +// const imageURIs: string[] = []; + +// addedHats.forEach(hat => { +// admins.push(BigInt(adminHatId)); +// details.push(hat.details); +// maxSupplies.push(hat.maxSupply); +// eligibilityModules.push(topHatAccount); +// toggleModules.push(topHatAccount); +// mutables.push(hat.isMutable); +// imageURIs.push(hat.imageURI); +// }); + +// return [ +// admins, +// details, +// maxSupplies, +// eligibilityModules, +// toggleModules, +// mutables, +// imageURIs, +// ] as const; +// }; export default function useCreateRoles() { const { @@ -326,8 +338,12 @@ export default function useCreateRoles() { const { t } = useTranslation(['roles', 'navigation', 'modals', 'common']); const { submitProposal } = useSubmitProposal(); - const { prepareBatchLinearStreamCreation, prepareFlushStreamTx, prepareCancelStreamTx } = - useCreateSablierStream(); + const { + prepareLinearStream, + // prepareBatchLinearStreamCreation, + // prepareFlushStreamTx, + // prepareCancelStreamTx, + } = useCreateSablierStream(); const ipfsClient = useIPFSClient(); const publicClient = usePublicClient(); const navigate = useNavigate(); @@ -446,355 +462,520 @@ export default function useCreateRoles() { ], ); - const prepareHatsAccountFlushExecData = useCallback( - (streamId: string, contractAddress: Address, wearer: Address) => { - const flushStreamTxCalldata = prepareFlushStreamTx(streamId, wearer); - const wrappedFlushStreamTx = encodeFunctionData({ - abi: HatsAccount1ofNAbi, - functionName: 'execute', - args: [contractAddress, 0n, flushStreamTxCalldata, 0], - }); + // const prepareHatsAccountFlushExecData = useCallback( + // (streamId: string, contractAddress: Address, wearer: Address) => { + // const flushStreamTxCalldata = prepareFlushStreamTx(streamId, wearer); + // const wrappedFlushStreamTx = encodeFunctionData({ + // abi: HatsAccount1ofNAbi, + // functionName: 'execute', + // args: [contractAddress, 0n, flushStreamTxCalldata, 0], + // }); + + // return wrappedFlushStreamTx; + // }, + // [prepareFlushStreamTx], + // ); + + // const prepareHatFlushAndCancelPayment = useCallback( + // (streamId: string, contractAddress: Address, wearer: Address) => { + // const cancelStreamTx = prepareCancelStreamTx(streamId, contractAddress); + // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + // streamId, + // contractAddress, + // wearer, + // ); + + // return { wrappedFlushStreamTx, cancelStreamTx }; + // }, + // [prepareCancelStreamTx, prepareHatsAccountFlushExecData], + // ); + + // const prepareEditHatsProposalData = useCallback( + // ( + // proposalMetadata: CreateProposalMetadata, + // addedHats: PreparedAddedHatsData[], + // removedHatIds: Hex[], + // memberChangedHats: PreparedMemberChangeData[], + // roleDetailsChangedHats: PreparedChangedRoleDetailsData[], + // editedPaymentStreams: PreparedEditedStreamData[], + // addedPaymentStreams: PreparedNewStreamData[], + // ) => { + // if (!hatsTree || !daoAddress) { + // throw new Error('Can not edit hats without Hats Tree!'); + // } + + // const topHatAccount = hatsTree.topHat.smartAddress; + // const adminHatId = hatsTree.adminHat.id; + + // const createAndMintHatsTxs: Hex[] = []; + // let removeHatTxs: Hex[] = []; + // let transferHatTxs: Hex[] = []; + // let hatDetailsChangedTxs: Hex[] = []; + + // let smartAccountTxs: { calldata: Hex; targetAddress: Address }[] = []; + // let hatPaymentAddedTxs: { calldata: Hex; targetAddress: Address }[] = []; + // let hatPaymentEditedTxs: { calldata: Hex; targetAddress: Address }[] = []; + + // let hatPaymentWearerChangedTxs: { calldata: Hex; targetAddress: Address }[] = []; + // let hatPaymentHatRemovedTxs: { calldata: Hex; targetAddress: Address }[] = []; + + // // let paymentStreamFlushAndCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; + // // let paymentStreamFlushTxs: { calldata: Hex; targetAddress: Address }[] = []; + // // let paymentStreamCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; + // // @todo should not flush same stream more than once + // // @todo possibly remove duplicate flushes after transactions have been prepared + // if (addedHats.length) { + // // First, prepare a single tx to create all the hats + // const createHatsTx = encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'batchCreateHats', + // args: prepareAddHatsTxArgs(addedHats, adminHatId, topHatAccount), + // }); + + // const mintHatsTx = encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'batchMintHats', + // // @note hatIds[], wearers[] + // args: [addedHats.map(h => h.id), addedHats.map(h => h.wearer)], + // }); + + // // finally, finally create smart account for hats. + // const createSmartAccountCallDatas = addedHats.map(hat => { + // return encodeFunctionData({ + // abi: ERC6551RegistryAbi, + // functionName: 'createAccount', + // args: [ + // hatsAccount1ofNMasterCopy, + // getERC6551RegistrySalt(BigInt(chain.id), getAddress(decentHatsMasterCopy)), + // BigInt(chain.id), + // hatsProtocol, + // hat.id, + // ], + // }); + // }); + // // Push these two txs to the included txs array. + // // They will be executed in order: add all hats first, then mint all hats to their respective wearers. + // createAndMintHatsTxs.push(createHatsTx); + // createAndMintHatsTxs.push(mintHatsTx); + // smartAccountTxs.push( + // ...createSmartAccountCallDatas.map(calldata => ({ + // calldata, + // targetAddress: erc6551Registry, + // })), + // ); + // } + + // if (removedHatIds.length) { + // removedHatIds.forEach(hatId => { + // const roleHat = hatsTree.roleHats.find(hat => hat.id === hatId); + // if (roleHat && roleHat.payments?.length) { + // /** + // * Assumption: current state of blockchain + // * Fact: does the hat currently have funds to withdraw + // * Do: if yes, then flush the stream + // */ + + // const fundsToClaimStreams = roleHat.payments.filter( + // payment => payment.withdrawableAmount > 0n, + // ); + // if (fundsToClaimStreams.length) { + // hatPaymentHatRemovedTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [BigInt(hatId), roleHat.wearer, daoAddress], + // }), + // targetAddress: hatsProtocol, + // }); + // fundsToClaimStreams.forEach(payment => { + // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + // payment.streamId, + // payment.contractAddress, + // roleHat.wearer, + // ); + // hatPaymentHatRemovedTxs.push({ + // calldata: wrappedFlushStreamTx, + // targetAddress: roleHat.smartAddress, + // }); + // }); + // } + // /** + // * Assumption: current state of blockchain + // * Fact: does the hat currently have funds that are not active or have not ended + // * Do: if yes, then cancel the stream + // */ + // const streamsToCancel = roleHat.payments.filter( + // payment => !payment.isCancelled && payment.endDate > new Date(), + // ); + // streamsToCancel.forEach(payment => { + // hatPaymentHatRemovedTxs.push( + // prepareCancelStreamTx(payment.streamId, payment.contractAddress), + // ); + // }); + // if (streamsToCancel.length || fundsToClaimStreams.length) { + // hatPaymentHatRemovedTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [BigInt(hatId), daoAddress, roleHat.wearer], + // }), + // targetAddress: hatsProtocol, + // }); + // } + // } + + // // make transaction proxy through erc6551 contract + // removeHatTxs.push( + // encodeFunctionData({ + // abi: HatsAccount1ofNAbi, + // functionName: 'execute', + // args: [ + // hatsProtocol, + // 0n, + // encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'setHatStatus', + // args: [BigInt(hatId), false], + // }), + // 0, + // ], + // }), + // ); + // }); + // } + + // if (memberChangedHats.length) { + // memberChangedHats.map(({ id, currentWearer, newWearer }) => { + // const roleHat = hatsTree.roleHats.find(hat => hat.id === id); + // if (roleHat && roleHat.payments?.length) { + // /** + // * Assumption: current state of blockchain + // * Fact: does the hat currently have funds to withdraw + // * Do: if yes, then flush the stream + // */ + // const fundsToClaimStreams = roleHat.payments.filter( + // payment => payment.withdrawableAmount > 0n, + // ); + // if (fundsToClaimStreams.length) { + // hatPaymentWearerChangedTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [BigInt(id), currentWearer, daoAddress], + // }), + // targetAddress: hatsProtocol, + // }); + // fundsToClaimStreams.forEach(payment => { + // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + // payment.streamId, + // payment.contractAddress, + // roleHat.wearer, + // ); + // hatPaymentWearerChangedTxs.push({ + // calldata: wrappedFlushStreamTx, + // targetAddress: roleHat.smartAddress, + // }); + // }); + // hatPaymentWearerChangedTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [BigInt(id), daoAddress, newWearer], + // }), + // targetAddress: hatsProtocol, + // }); + // } + // } else { + // transferHatTxs.push( + // encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [BigInt(id), daoAddress, newWearer], + // }), + // ); + // } + // }); + // } + + // if (roleDetailsChangedHats.length) { + // hatDetailsChangedTxs = roleDetailsChangedHats.map(({ id, details }) => { + // return encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'changeHatDetails', + // args: [BigInt(id), details], + // }); + // }); + // } + + // if (addedPaymentStreams.length) { + // const preparedPaymentTransactions = prepareBatchLinearStreamCreation(addedPaymentStreams); + // hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); + // hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); + // } + + // if (editedPaymentStreams.length) { + // /** + // * Assumption: current state of blockchain + // * Fact: does the hat currently have funds to withdraw + // * Do: if yes, then flush the stream + // */ + // const paymentCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; + // editedPaymentStreams.forEach(paymentStream => { + // paymentCancelTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [paymentStream.roleHatId, paymentStream.roleHatWearer, daoAddress], + // }), + // targetAddress: hatsProtocol, + // }); + // const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( + // paymentStream.streamId, + // paymentStream.streamContractAddress, + // paymentStream.roleHatWearer, + // ); + // paymentCancelTxs.push({ + // calldata: wrappedFlushStreamTx, + // targetAddress: paymentStream.roleHatSmartAddress, + // }); + // paymentCancelTxs.push(cancelStreamTx); + // paymentCancelTxs.push({ + // calldata: encodeFunctionData({ + // abi: HatsAbi, + // functionName: 'transferHat', + // args: [paymentStream.roleHatId, daoAddress, paymentStream.roleHatWearer], + // }), + // targetAddress: hatsProtocol, + // }); + // }); + + // const preparedPaymentTransactions = prepareBatchLinearStreamCreation(editedPaymentStreams); + // hatPaymentEditedTxs.push(...paymentCancelTxs); + // hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); + // hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); + // } + // console.log('🚀 ~ createAndMintHatsTxs:', createAndMintHatsTxs); + // console.log('🚀 ~ transferHatTxs:', transferHatTxs); + // console.log('🚀 ~ ...hatPaymentWearerChangedTxs.map:', hatPaymentWearerChangedTxs); + // console.log('🚀 ~ hatDetailsChangedTxs:', hatDetailsChangedTxs); + // console.log('🚀 ~ hatPaymentAddedTxs:', hatPaymentAddedTxs); + // console.log('🚀 ~ hatPaymentEditedTxs:', hatPaymentEditedTxs); + // console.log('🚀 ~ smartAccountTxs:', smartAccountTxs); + // console.log('🚀 ~ removeHatTxs:', removeHatTxs); + // console.log('🚀 ~ hatPaymentHatRemovedTxs:', hatPaymentHatRemovedTxs); + // const proposalTransactions = { + // targets: [ + // ...createAndMintHatsTxs.map(() => hatsProtocol), + // ...smartAccountTxs.map(({ targetAddress }) => targetAddress), + // ...hatPaymentHatRemovedTxs.map(({ targetAddress }) => targetAddress), + // ...removeHatTxs.map(() => topHatAccount), + // ...hatPaymentWearerChangedTxs.map(({ targetAddress }) => targetAddress), + // ...transferHatTxs.map(() => hatsProtocol), + // ...hatDetailsChangedTxs.map(() => hatsProtocol), + // ...hatPaymentAddedTxs.map(({ targetAddress }) => targetAddress), + // ...hatPaymentEditedTxs.map(({ targetAddress }) => targetAddress), + // ], + // calldatas: [ + // ...createAndMintHatsTxs, + // ...smartAccountTxs.map(({ calldata }) => calldata), + // ...hatPaymentHatRemovedTxs.map(({ calldata }) => calldata), + // ...removeHatTxs, + // ...hatPaymentWearerChangedTxs.map(({ calldata }) => calldata), + // ...transferHatTxs, + // ...hatDetailsChangedTxs, + // ...hatPaymentAddedTxs.map(({ calldata }) => calldata), + // ...hatPaymentEditedTxs.map(({ calldata }) => calldata), + // ], + // metaData: proposalMetadata, + // values: [ + // ...createAndMintHatsTxs.map(() => 0n), + // ...smartAccountTxs.map(() => 0n), + // ...hatPaymentHatRemovedTxs.map(() => 0n), + // ...removeHatTxs.map(() => 0n), + // ...hatPaymentWearerChangedTxs.map(() => 0n), + // ...transferHatTxs.map(() => 0n), + // ...hatDetailsChangedTxs.map(() => 0n), + // ...hatPaymentAddedTxs.map(() => 0n), + // ...hatPaymentEditedTxs.map(() => 0n), + // ], + // }; + + // return proposalTransactions; + // }, + // [ + // hatsProtocol, + // hatsTree, + // prepareCancelStreamTx, + // prepareHatFlushAndCancelPayment, + // prepareHatsAccountFlushExecData, + // daoAddress, + // prepareBatchLinearStreamCreation, + // hatsAccount1ofNMasterCopy, + // chain.id, + // decentHatsMasterCopy, + // erc6551Registry, + // ], + // ); + + const createHatTx = useCallback( + async (formRole: RoleHatFormValueEdited, adminHatId: bigint, topHatSmartAccount: Address) => { + if (formRole.name === undefined || formRole.description === undefined) { + throw new Error('Name or description of added Role is undefined.'); + } - return wrappedFlushStreamTx; - }, - [prepareFlushStreamTx], - ); + if (formRole.wearer === undefined) { + throw new Error('Member of added Role is undefined.'); + } - const prepareHatFlushAndCancelPayment = useCallback( - (streamId: string, contractAddress: Address, wearer: Address) => { - const cancelStreamTx = prepareCancelStreamTx(streamId, contractAddress); - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - streamId, - contractAddress, - wearer, + const hatStruct = await createHatStruct( + formRole.name, + formRole.description, + getAddress(formRole.wearer), + uploadHatDescription, ); - return { wrappedFlushStreamTx, cancelStreamTx }; + return { + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'createHat', + args: [ + adminHatId, // adminHatId + hatStruct.details, // details + hatStruct.maxSupply, // maxSupply + topHatSmartAccount, // eligibilityModule + topHatSmartAccount, // toggleModule + hatStruct.isMutable, // isMutable + hatStruct.wearer, // wearer + ], + }), + targetAddress: hatsProtocol, + }; }, - [prepareCancelStreamTx, prepareHatsAccountFlushExecData], + [uploadHatDescription, hatsProtocol], ); - const prepareEditHatsProposalData = useCallback( - ( - proposalMetadata: CreateProposalMetadata, - addedHats: PreparedAddedHatsData[], - removedHatIds: Hex[], - memberChangedHats: PreparedMemberChangeData[], - roleDetailsChangedHats: PreparedChangedRoleDetailsData[], - editedPaymentStreams: PreparedEditedStreamData[], - addedPaymentStreams: PreparedNewStreamData[], - ) => { - if (!hatsTree || !daoAddress) { - throw new Error('Can not edit hats without Hats Tree!'); + const mintHatTx = useCallback( + (newHatId: bigint, formHat: RoleHatFormValueEdited) => { + if (formHat.wearer === undefined) { + throw new Error('Hat wearer of added hat is undefined.'); } - const topHatAccount = hatsTree.topHat.smartAddress; - const adminHatId = hatsTree.adminHat.id; - - const createAndMintHatsTxs: Hex[] = []; - let removeHatTxs: Hex[] = []; - let transferHatTxs: Hex[] = []; - let hatDetailsChangedTxs: Hex[] = []; + return { + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'mintHat', + args: [newHatId, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }; + }, + [hatsProtocol], + ); - let smartAccountTxs: { calldata: Hex; targetAddress: Address }[] = []; - let hatPaymentAddedTxs: { calldata: Hex; targetAddress: Address }[] = []; - let hatPaymentEditedTxs: { calldata: Hex; targetAddress: Address }[] = []; + const createSmartAccountTx = useCallback( + (newHatId: bigint) => { + return { + calldata: encodeFunctionData({ + abi: ERC6551RegistryAbi, + functionName: 'createAccount', + args: [ + hatsAccount1ofNMasterCopy, + getERC6551RegistrySalt(BigInt(chain.id), getAddress(decentHatsMasterCopy)), + BigInt(chain.id), + hatsProtocol, + newHatId, + ], + }), + targetAddress: erc6551Registry, + }; + }, + [chain.id, decentHatsMasterCopy, erc6551Registry, hatsAccount1ofNMasterCopy, hatsProtocol], + ); - let hatPaymentWearerChangedTxs: { calldata: Hex; targetAddress: Address }[] = []; - let hatPaymentHatRemovedTxs: { calldata: Hex; targetAddress: Address }[] = []; + const prepareAllTxs = useCallback(async (modifiedHats: RoleHatFormValueEdited[]) => { + if (!hatsTree) { + throw new Error('Cannot prepare transactions'); + } - if (addedHats.length) { - // First, prepare a single tx to create all the hats - const createHatsTx = encodeFunctionData({ - abi: HatsAbi, - functionName: 'batchCreateHats', - args: prepareAddHatsTxArgs(addedHats, adminHatId, topHatAccount), - }); + const topHatAccount = hatsTree.topHat.smartAddress; + const adminHatId = BigInt(hatsTree.adminHat.id); - const mintHatsTx = encodeFunctionData({ - abi: HatsAbi, - functionName: 'batchMintHats', - // @note hatIds[], wearers[] - args: [addedHats.map(h => h.id), addedHats.map(h => h.wearer)], - }); + const allTxs: { calldata: Hex; targetAddress: Address }[] = []; - // finally, finally create smart account for hats. - const createSmartAccountCallDatas = addedHats.map(hat => { - return encodeFunctionData({ - abi: ERC6551RegistryAbi, - functionName: 'createAccount', - args: [ - hatsAccount1ofNMasterCopy, - getERC6551RegistrySalt(BigInt(chain.id), getAddress(decentHatsMasterCopy)), - BigInt(chain.id), - hatsProtocol, - hat.id, - ], - }); - }); - // Push these two txs to the included txs array. - // They will be executed in order: add all hats first, then mint all hats to their respective wearers. - createAndMintHatsTxs.push(createHatsTx); - createAndMintHatsTxs.push(mintHatsTx); - smartAccountTxs.push( - ...createSmartAccountCallDatas.map(calldata => ({ - calldata, - targetAddress: erc6551Registry, - })), - ); - } + // we need to keep track of how many new hats there are, + // so that we can correctly predict the hatId for the "create new role" transaction + let newHatCount = 0; - if (removedHatIds.length) { - removedHatIds.forEach(hatId => { - const roleHat = hatsTree.roleHats.find(hat => hat.id === hatId); - if (roleHat && roleHat.payments?.length) { - /** - * Assumption: current state of blockchain - * Fact: does the hat currently have funds to withdraw - * Do: if yes, then flush the stream - */ - - const fundsToClaimStreams = roleHat.payments.filter( - payment => payment.withdrawableAmount > 0n, - ); - if (fundsToClaimStreams.length) { - hatPaymentHatRemovedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(hatId), roleHat.wearer, daoAddress], - }), - targetAddress: hatsProtocol, - }); - fundsToClaimStreams.forEach(payment => { - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - payment.streamId, - payment.contractAddress, - roleHat.wearer, - ); - hatPaymentHatRemovedTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: roleHat.smartAddress, - }); - }); - } - /** - * Assumption: current state of blockchain - * Fact: does the hat currently have funds that are not active or have not ended - * Do: if yes, then cancel the stream - */ - const streamsToCancel = roleHat.payments.filter( - payment => !payment.isCancelled && payment.endDate > new Date(), - ); - streamsToCancel.forEach(payment => { - hatPaymentHatRemovedTxs.push( - prepareCancelStreamTx(payment.streamId, payment.contractAddress), - ); - }); - if (streamsToCancel.length || fundsToClaimStreams.length) { - hatPaymentHatRemovedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(hatId), daoAddress, roleHat.wearer], - }), - targetAddress: hatsProtocol, - }); - } - } + // "active stream" = not cancelled and not past end date + // "inactive stream" = cancelled or past end date - // make transaction proxy through erc6551 contract - removeHatTxs.push( - encodeFunctionData({ - abi: HatsAccount1ofNAbi, - functionName: 'execute', - args: [ - hatsProtocol, - 0n, - encodeFunctionData({ - abi: HatsAbi, - functionName: 'setHatStatus', - args: [BigInt(hatId), false], - }), - 0, - ], - }), - ); - }); - } + // for each modified role + for (let index = 0; index < modifiedHats.length; index++) { + const formHat = modifiedHats[index]; - if (memberChangedHats.length) { - memberChangedHats.map(({ id, currentWearer, newWearer }) => { - const roleHat = hatsTree.roleHats.find(hat => hat.id === id); - if (roleHat && roleHat.payments?.length) { - /** - * Assumption: current state of blockchain - * Fact: does the hat currently have funds to withdraw - * Do: if yes, then flush the stream - */ - const fundsToClaimStreams = roleHat.payments.filter( - payment => payment.withdrawableAmount > 0n, - ); - if (fundsToClaimStreams.length) { - hatPaymentWearerChangedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), currentWearer, daoAddress], - }), - targetAddress: hatsProtocol, - }); - fundsToClaimStreams.forEach(payment => { - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - payment.streamId, - payment.contractAddress, - roleHat.wearer, - ); - hatPaymentWearerChangedTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: roleHat.smartAddress, - }); - }); - hatPaymentWearerChangedTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), daoAddress, newWearer], - }), - targetAddress: hatsProtocol, - }); - } - } else { - transferHatTxs.push( - encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(id), daoAddress, newWearer], - }), - ); - } - }); - } + if (formHat.editedRole.status === EditBadgeStatus.New) { + // New Role + // - "create new role" transaction data + // - includes create hat, mint hat, create smart account - if (roleDetailsChangedHats.length) { - hatDetailsChangedTxs = roleDetailsChangedHats.map(({ id, details }) => { - return encodeFunctionData({ - abi: HatsAbi, - functionName: 'changeHatDetails', - args: [BigInt(id), details], - }); + const newHatId = predictHatId({ + adminHatId: hatsTree.adminHat.id, + hatsCount: hatsTree.roleHatsTotalCount + newHatCount, }); - } + newHatCount++; - if (addedPaymentStreams.length) { - const preparedPaymentTransactions = prepareBatchLinearStreamCreation(addedPaymentStreams); - hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); - hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); - } + allTxs.push(await createHatTx(formHat, adminHatId, topHatAccount)); + allTxs.push(mintHatTx(newHatId, formHat)); + allTxs.push(createSmartAccountTx(BigInt(newHatId))); - if (editedPaymentStreams.length) { - /** - * Assumption: current state of blockchain - * Fact: does the hat currently have funds to withdraw - * Do: if yes, then flush the stream - */ - const paymentCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; - editedPaymentStreams.forEach(paymentStream => { - const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( - paymentStream.streamId, - paymentStream.streamContractAddress, - paymentStream.roleHatWearer, - ); - paymentCancelTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [paymentStream.roleHatId, paymentStream.roleHatWearer, daoAddress], - }), - targetAddress: hatsProtocol, - }); - paymentCancelTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: paymentStream.roleHatSmartAddress, - }); - paymentCancelTxs.push(cancelStreamTx); - paymentCancelTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [paymentStream.roleHatId, daoAddress, paymentStream.roleHatWearer], - }), - targetAddress: hatsProtocol, - }); - }); + // - does it have any streams? + if (formHat.payments !== undefined && formHat.payments.length > 0) { + // - allTxs.push(create new streams transactions datas) + // allTxs.push(...prepareBatchLinearStreamCreation(formHat.payments)); - const preparedPaymentTransactions = prepareBatchLinearStreamCreation(editedPaymentStreams); - hatPaymentEditedTxs.push(...paymentCancelTxs); - hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); - hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); + // @david i'm here + const foo = prepareLinearStream(formHat.payments[0]); + allTxs.push(); + } + } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { + // Deleted Role + // - does it have any inactive streams which have funds to claim? + // - allTxs.push(flush stream transaction data) + // - does it have any active streams? + // - allTxs.push(flush and cancel stream transactions data) + // - allTxs.push(deactivate role transaction data) + } else { + // Edited Role + // - else + // - is the name or description changed? + // - allTxs.push(edit details data) + // - is the member changed? + // - does it have any inactive streams which have funds to claim? + // - allTxs.push(flush stream transaction data) + // - for each active stream + // - if stream was edited, too + // - skip + // - else + // - allTxs.push(flush stream transaction data) + // - for each active streams + // - if stream was edited + // - allTxs.push(flush and cancel stream transaction data) + // - allTxs.push(create new stream transaction data) } + } - const proposalTransactions = { - targets: [ - ...createAndMintHatsTxs.map(() => hatsProtocol), - ...smartAccountTxs.map(({ targetAddress }) => targetAddress), - ...hatPaymentHatRemovedTxs.map(({ targetAddress }) => targetAddress), - ...removeHatTxs.map(() => topHatAccount), - ...hatPaymentWearerChangedTxs.map(({ targetAddress }) => targetAddress), - ...transferHatTxs.map(() => hatsProtocol), - ...hatDetailsChangedTxs.map(() => hatsProtocol), - ...hatPaymentAddedTxs.map(({ targetAddress }) => targetAddress), - ...hatPaymentEditedTxs.map(({ targetAddress }) => targetAddress), - ], - calldatas: [ - ...createAndMintHatsTxs, - ...smartAccountTxs.map(({ calldata }) => calldata), - ...hatPaymentHatRemovedTxs.map(({ calldata }) => calldata), - ...removeHatTxs, - ...hatPaymentWearerChangedTxs.map(({ calldata }) => calldata), - ...transferHatTxs, - ...hatDetailsChangedTxs, - ...hatPaymentAddedTxs.map(({ calldata }) => calldata), - ...hatPaymentEditedTxs.map(({ calldata }) => calldata), - ], - metaData: proposalMetadata, - values: [ - ...createAndMintHatsTxs.map(() => 0n), - ...smartAccountTxs.map(() => 0n), - ...hatPaymentHatRemovedTxs.map(() => 0n), - ...removeHatTxs.map(() => 0n), - ...hatPaymentWearerChangedTxs.map(() => 0n), - ...transferHatTxs.map(() => 0n), - ...hatDetailsChangedTxs.map(() => 0n), - ...hatPaymentAddedTxs.map(() => 0n), - ...hatPaymentEditedTxs.map(() => 0n), - ], - }; - - return proposalTransactions; - }, - [ - hatsProtocol, - hatsTree, - prepareCancelStreamTx, - prepareHatFlushAndCancelPayment, - prepareHatsAccountFlushExecData, - daoAddress, - prepareBatchLinearStreamCreation, - hatsAccount1ofNMasterCopy, - chain.id, - decentHatsMasterCopy, - erc6551Registry, - ], - ); + return allTxs; + }, []); const createEditRolesProposal = useCallback( async (values: RoleFormValues, formikHelpers: FormikHelpers) => { if (!publicClient) { throw new Error('Cannot create Roles proposal without public client'); } + const { setSubmitting } = formikHelpers; setSubmitting(true); + if (!safe) { setSubmitting(false); throw new Error('Cannot create Roles proposal without known Safe'); @@ -810,68 +991,85 @@ export default function useCreateRoles() { }) .filter(hat => hat !== null); - // find all new hats and prepare them for creation + let proposalData: ProposalExecuteData; try { - const [addedHats, addedHatsRolesValues] = await identifyAndPrepareAddedHats( - modifiedHats, - uploadHatDescription, - ); - - let proposalData: ProposalExecuteData; - if (!hatsTreeId) { // This safe has no top hat, so we prepare a proposal to create one. This will also create an admin hat, // along with any other hats that are added. - proposalData = await prepareCreateTopHatProposalData(values.proposalMetadata, addedHats); - } else { - if (!hatsTree) { - throw new Error('Cannot edit Roles without a HatsTree'); + + if (modifiedHats.some(hat => hat.editedRole.status !== EditBadgeStatus.New)) { + throw new Error( + 'No Hats Tree ID exists, but some modified Roles are marked as non-New.', + ); } - // Convert addedHats to include predicted id - const addedHatsWithIds = await Promise.all( - addedHatsRolesValues.map(async (hat, index) => { - const hatId = predictHatId({ - adminHatId: hatsTree.adminHat.id, - hatsCount: hatsTree.roleHats.length + index, - }); - return { - ...hat, - id: hatId, - // @note For new hats, a randomId is created for temporary indentification - formId: hat.id, - }; - }), - ); - const removedHatIds = identifyAndPrepareRemovedHats(modifiedHats); - const memberChangedHats = identifyAndPrepareMemberChangedHats(modifiedHats, getHat); - const roleDetailsChangedHats = await identifyAndPrepareRoleDetailsChangedHats( + const newHatStructs = await createHatStructsFromRolesFormValues( modifiedHats, uploadHatDescription, - getHat, ); - const editedPaymentStreams = identifyAndPrepareEditedPaymentStreams( - modifiedHats, - getHat, - getPayment, + proposalData = await prepareCreateTopHatProposalData( + values.proposalMetadata, + newHatStructs, ); + } else { + if (!hatsTree) { + throw new Error('Cannot edit Roles without a HatsTree'); + } - const addedPaymentStreamsOnNewHats = await identifyAndPrepareAddedPaymentStreams( - modifiedHats, - addedHatsWithIds, - getHat, - predictSmartAccount, - ); + const allTxs = await prepareAllTxs(modifiedHats); - proposalData = prepareEditHatsProposalData( - values.proposalMetadata, - addedHats.map((hat, index) => ({ ...hat, id: addedHatsWithIds[index].id })), - removedHatIds, - memberChangedHats, - roleDetailsChangedHats, - editedPaymentStreams, - addedPaymentStreamsOnNewHats, - ); + // Convert addedHats to include predicted id + // const addedHatsWithIds = await Promise.all( + // addedHatsRolesValues.map(async (hat, index) => { + // const hatId = predictHatId({ + // adminHatId: hatsTree.adminHat.id, + // hatsCount: hatsTree.roleHatsTotalCount + index, + // }); + // return { + // ...hat, + // id: hatId, + // // @note For new hats, a randomId is created for temporary indentification + // formId: hat.id, + // }; + // }), + // ); + + // const removedHatIds = identifyAndPrepareRemovedHats(modifiedHats); + // const memberChangedHats = identifyAndPrepareMemberChangedHats(modifiedHats, getHat); + // const roleDetailsChangedHats = await identifyAndPrepareRoleDetailsChangedHats( + // modifiedHats, + // uploadHatDescription, + // getHat, + // ); + // const editedPaymentStreams = identifyAndPrepareEditedPaymentStreams( + // modifiedHats, + // getHat, + // getPayment, + // ); + + // const addedPaymentStreamsOnNewHats = await identifyAndPrepareAddedPaymentStreams( + // modifiedHats, + // addedHatsWithIds, + // getHat, + // predictSmartAccount, + // ); + + // proposalData = prepareEditHatsProposalData( + // values.proposalMetadata, + // addedHats.map((hat, index) => ({ ...hat, id: addedHatsWithIds[index].id })), + // removedHatIds, + // memberChangedHats, + // roleDetailsChangedHats, + // editedPaymentStreams, + // addedPaymentStreamsOnNewHats, + // ); + + proposalData = { + targets: allTxs.map(({ targetAddress }) => targetAddress), + calldatas: allTxs.map(({ calldata }) => calldata), + values: allTxs.map(() => 0n), + metaData: values.proposalMetadata, + }; } // All done, submit the proposal! @@ -890,21 +1088,7 @@ export default function useCreateRoles() { formikHelpers.setSubmitting(false); } }, - [ - safe, - hatsTreeId, - hatsTree, - uploadHatDescription, - predictSmartAccount, - getHat, - getPayment, - submitProposal, - submitProposalSuccessCallback, - prepareCreateTopHatProposalData, - prepareEditHatsProposalData, - publicClient, - t, - ], + [], ); return { From 737b135cbcc9287561c052c2f658d9d8bd0c9c5b Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:32:21 -0400 Subject: [PATCH 09/58] adds tx: - new role - new stream --- src/hooks/utils/useCreateRoles.ts | 62 ++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index dfbf5f090..2eae06f0e 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -339,8 +339,7 @@ export default function useCreateRoles() { const { submitProposal } = useSubmitProposal(); const { - prepareLinearStream, - // prepareBatchLinearStreamCreation, + prepareBatchLinearStreamCreation, // prepareFlushStreamTx, // prepareCancelStreamTx, } = useCreateSablierStream(); @@ -929,13 +928,37 @@ export default function useCreateRoles() { allTxs.push(createSmartAccountTx(BigInt(newHatId))); // - does it have any streams? - if (formHat.payments !== undefined && formHat.payments.length > 0) { - // - allTxs.push(create new streams transactions datas) - // allTxs.push(...prepareBatchLinearStreamCreation(formHat.payments)); + const newStreams = + !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); + if (!!newStreams && newStreams.length > 0) { + const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); + const preparedNewStreams = newStreams.map(stream => { + if ( + !stream.asset || + !stream.startDate || + !stream.endDate || + !stream.amount?.bigintValue || + stream.amount.bigintValue <= 0n + ) { + throw new Error('Form Values inValid', { + cause: stream, + }); + } + + return { + recipient: newPredictedHatSmartAccount, + startDateTs: Math.floor(stream.startDate.getTime() / 1000), + endDateTs: Math.ceil(stream.endDate.getTime() / 1000), + cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), + totalAmount: stream.amount.bigintValue, + assetAddress: stream.asset.address, + }; + }); - // @david i'm here - const foo = prepareLinearStream(formHat.payments[0]); - allTxs.push(); + // - allTxs.push(create new streams transactions datas) + const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); + allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); + allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { // Deleted Role @@ -945,7 +968,7 @@ export default function useCreateRoles() { // - allTxs.push(flush and cancel stream transactions data) // - allTxs.push(deactivate role transaction data) } else { - // Edited Role + // Edited Role (existing role) // - else // - is the name or description changed? // - allTxs.push(edit details data) @@ -961,6 +984,8 @@ export default function useCreateRoles() { // - if stream was edited // - allTxs.push(flush and cancel stream transaction data) // - allTxs.push(create new stream transaction data) + // - for each new streams + // - allTxs.push(create new stream transaction data) } } @@ -1017,6 +1042,7 @@ export default function useCreateRoles() { } const allTxs = await prepareAllTxs(modifiedHats); + console.log("🚀 ~ allTxs:", allTxs) // Convert addedHats to include predicted id // const addedHatsWithIds = await Promise.all( @@ -1072,15 +1098,15 @@ export default function useCreateRoles() { }; } - // All done, submit the proposal! - await submitProposal({ - proposalData, - nonce: values.customNonce ?? safe.nextNonce, - pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), - successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), - failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - successCallback: submitProposalSuccessCallback, - }); + // // All done, submit the proposal! + // await submitProposal({ + // proposalData, + // nonce: values.customNonce ?? safe.nextNonce, + // pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), + // successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), + // failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), + // successCallback: submitProposalSuccessCallback, + // }); } catch (e) { console.error(e); toast(t('encodingFailedMessage', { ns: 'proposal' })); From d65140073832192a480e7579144993eb1116e6e5 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:28:32 -0400 Subject: [PATCH 10/58] handle remove hat transactions --- src/hooks/utils/useCreateRoles.ts | 141 +++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 33 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 2eae06f0e..e44481c92 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -338,11 +338,8 @@ export default function useCreateRoles() { const { t } = useTranslation(['roles', 'navigation', 'modals', 'common']); const { submitProposal } = useSubmitProposal(); - const { - prepareBatchLinearStreamCreation, - // prepareFlushStreamTx, - // prepareCancelStreamTx, - } = useCreateSablierStream(); + const { prepareBatchLinearStreamCreation, prepareFlushStreamTx, prepareCancelStreamTx } = + useCreateSablierStream(); const ipfsClient = useIPFSClient(); const publicClient = usePublicClient(); const navigate = useNavigate(); @@ -461,33 +458,33 @@ export default function useCreateRoles() { ], ); - // const prepareHatsAccountFlushExecData = useCallback( - // (streamId: string, contractAddress: Address, wearer: Address) => { - // const flushStreamTxCalldata = prepareFlushStreamTx(streamId, wearer); - // const wrappedFlushStreamTx = encodeFunctionData({ - // abi: HatsAccount1ofNAbi, - // functionName: 'execute', - // args: [contractAddress, 0n, flushStreamTxCalldata, 0], - // }); + const prepareHatsAccountFlushExecData = useCallback( + (streamId: string, contractAddress: Address, wearer: Address) => { + const flushStreamTxCalldata = prepareFlushStreamTx(streamId, wearer); + const wrappedFlushStreamTx = encodeFunctionData({ + abi: HatsAccount1ofNAbi, + functionName: 'execute', + args: [contractAddress, 0n, flushStreamTxCalldata, 0], + }); - // return wrappedFlushStreamTx; - // }, - // [prepareFlushStreamTx], - // ); + return wrappedFlushStreamTx; + }, + [prepareFlushStreamTx], + ); - // const prepareHatFlushAndCancelPayment = useCallback( - // (streamId: string, contractAddress: Address, wearer: Address) => { - // const cancelStreamTx = prepareCancelStreamTx(streamId, contractAddress); - // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - // streamId, - // contractAddress, - // wearer, - // ); + const prepareHatFlushAndCancelPayment = useCallback( + (streamId: string, contractAddress: Address, wearer: Address) => { + const cancelStreamTx = prepareCancelStreamTx(streamId, contractAddress); + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + streamId, + contractAddress, + wearer, + ); - // return { wrappedFlushStreamTx, cancelStreamTx }; - // }, - // [prepareCancelStreamTx, prepareHatsAccountFlushExecData], - // ); + return { wrappedFlushStreamTx, cancelStreamTx }; + }, + [prepareCancelStreamTx, prepareHatsAccountFlushExecData], + ); // const prepareEditHatsProposalData = useCallback( // ( @@ -892,7 +889,7 @@ export default function useCreateRoles() { ); const prepareAllTxs = useCallback(async (modifiedHats: RoleHatFormValueEdited[]) => { - if (!hatsTree) { + if (!hatsTree || !daoAddress) { throw new Error('Cannot prepare transactions'); } @@ -962,11 +959,89 @@ export default function useCreateRoles() { } } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { // Deleted Role + if (formHat.wearer === undefined || formHat.smartAddress === undefined) { + throw new Error('Cannot prepare transactions for removed role without wearer'); + } + // - does it have any inactive streams which have funds to claim? - // - allTxs.push(flush stream transaction data) + const fundsToClaimStreams = formHat?.payments?.filter( + payment => (payment?.withdrawableAmount ?? 0n) > 0n, + ); + if (fundsToClaimStreams && fundsToClaimStreams.length) { + // - allTxs.push(flush stream transaction data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); + for (const stream of fundsToClaimStreams) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + getAddress(formHat.wearer), + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); + } + } + // - does it have any active streams? - // - allTxs.push(flush and cancel stream transactions data) + const streamsToCancel = formHat?.payments?.filter( + payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), + ); + if (!!streamsToCancel && streamsToCancel.length) { + // - allTxs.push(cancel stream transactions data) + for (const stream of streamsToCancel) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for cancel stream transaction', + ); + } + allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); + } + } + if ( + (streamsToCancel && streamsToCancel.length) || + (fundsToClaimStreams && fundsToClaimStreams.length) + ) { + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } + // - allTxs.push(deactivate role transaction data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAccount1ofNAbi, + functionName: 'execute', + args: [ + hatsProtocol, + 0n, + encodeFunctionData({ + abi: HatsAbi, + functionName: 'setHatStatus', + args: [BigInt(formHat.id), false], + }), + 0, + ], + }), + targetAddress: topHatAccount, + }); } else { // Edited Role (existing role) // - else @@ -1042,7 +1117,7 @@ export default function useCreateRoles() { } const allTxs = await prepareAllTxs(modifiedHats); - console.log("🚀 ~ allTxs:", allTxs) + console.log('🚀 ~ allTxs:', allTxs); // Convert addedHats to include predicted id // const addedHatsWithIds = await Promise.all( From 239a312c26797b50c03d55aa7e356b6bffb37786 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:50:18 -0400 Subject: [PATCH 11/58] handle editted hats --- src/hooks/utils/useCreateRoles.ts | 531 ++++++++++++++++++++---------- 1 file changed, 365 insertions(+), 166 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index e44481c92..2e3af339b 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -12,17 +12,17 @@ import GnosisSafeL2 from '../../assets/abi/GnosisSafeL2'; import { HatsAbi } from '../../assets/abi/HatsAbi'; import HatsAccount1ofNAbi from '../../assets/abi/HatsAccount1ofN'; import { - AddedHatsWithIds, + // AddedHatsWithIds, EditBadgeStatus, HatStruct, - PreparedAddedHatsData, - PreparedChangedRoleDetailsData, - PreparedMemberChangeData, - PreparedNewStreamData, - PreparedEditedStreamData, + // PreparedAddedHatsData, + // PreparedChangedRoleDetailsData, + // PreparedMemberChangeData, + // PreparedNewStreamData, + // PreparedEditedStreamData, RoleFormValues, RoleHatFormValueEdited, - SablierPayment, + // SablierPayment, } from '../../components/pages/Roles/types'; import { DAO_ROUTES } from '../../constants/routes'; import { useFractal } from '../../providers/App/AppProvider'; @@ -33,7 +33,7 @@ import { CreateProposalMetadata, ProposalExecuteData } from '../../types'; import { SENTINEL_MODULE } from '../../utils/address'; import useSubmitProposal from '../DAO/proposal/useSubmitProposal'; import useCreateSablierStream from '../streams/useCreateSablierStream'; -import { DecentRoleHat, predictAccountAddress } from './../../store/roles/rolesStoreUtils'; +import { predictAccountAddress } from './../../store/roles/rolesStoreUtils'; const hatsDetailsBuilder = (data: { name: string; description: string }) => { return JSON.stringify({ @@ -888,184 +888,383 @@ export default function useCreateRoles() { [chain.id, decentHatsMasterCopy, erc6551Registry, hatsAccount1ofNMasterCopy, hatsProtocol], ); - const prepareAllTxs = useCallback(async (modifiedHats: RoleHatFormValueEdited[]) => { - if (!hatsTree || !daoAddress) { - throw new Error('Cannot prepare transactions'); - } + const prepareAllTxs = useCallback( + async (modifiedHats: RoleHatFormValueEdited[]) => { + if (!hatsTree || !daoAddress) { + throw new Error('Cannot prepare transactions'); + } - const topHatAccount = hatsTree.topHat.smartAddress; - const adminHatId = BigInt(hatsTree.adminHat.id); - - const allTxs: { calldata: Hex; targetAddress: Address }[] = []; - - // we need to keep track of how many new hats there are, - // so that we can correctly predict the hatId for the "create new role" transaction - let newHatCount = 0; - - // "active stream" = not cancelled and not past end date - // "inactive stream" = cancelled or past end date - - // for each modified role - for (let index = 0; index < modifiedHats.length; index++) { - const formHat = modifiedHats[index]; - - if (formHat.editedRole.status === EditBadgeStatus.New) { - // New Role - // - "create new role" transaction data - // - includes create hat, mint hat, create smart account - - const newHatId = predictHatId({ - adminHatId: hatsTree.adminHat.id, - hatsCount: hatsTree.roleHatsTotalCount + newHatCount, - }); - newHatCount++; - - allTxs.push(await createHatTx(formHat, adminHatId, topHatAccount)); - allTxs.push(mintHatTx(newHatId, formHat)); - allTxs.push(createSmartAccountTx(BigInt(newHatId))); - - // - does it have any streams? - const newStreams = - !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); - if (!!newStreams && newStreams.length > 0) { - const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); - const preparedNewStreams = newStreams.map(stream => { - if ( - !stream.asset || - !stream.startDate || - !stream.endDate || - !stream.amount?.bigintValue || - stream.amount.bigintValue <= 0n - ) { - throw new Error('Form Values inValid', { - cause: stream, - }); - } + const topHatAccount = hatsTree.topHat.smartAddress; + const adminHatId = BigInt(hatsTree.adminHat.id); - return { - recipient: newPredictedHatSmartAccount, - startDateTs: Math.floor(stream.startDate.getTime() / 1000), - endDateTs: Math.ceil(stream.endDate.getTime() / 1000), - cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: stream.amount.bigintValue, - assetAddress: stream.asset.address, - }; - }); + const allTxs: { calldata: Hex; targetAddress: Address }[] = []; - // - allTxs.push(create new streams transactions datas) - const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); - allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); - allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); - } - } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { - // Deleted Role - if (formHat.wearer === undefined || formHat.smartAddress === undefined) { - throw new Error('Cannot prepare transactions for removed role without wearer'); - } + // we need to keep track of how many new hats there are, + // so that we can correctly predict the hatId for the "create new role" transaction + let newHatCount = 0; - // - does it have any inactive streams which have funds to claim? - const fundsToClaimStreams = formHat?.payments?.filter( - payment => (payment?.withdrawableAmount ?? 0n) > 0n, - ); - if (fundsToClaimStreams && fundsToClaimStreams.length) { - // - allTxs.push(flush stream transaction data) - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], - }), - targetAddress: hatsProtocol, + // "active stream" = not cancelled and not past end date + // "inactive stream" = cancelled or past end date + + // for each modified role + for (let index = 0; index < modifiedHats.length; index++) { + const formHat = modifiedHats[index]; + + if (formHat.editedRole.status === EditBadgeStatus.New) { + // New Role + // - "create new role" transaction data + // - includes create hat, mint hat, create smart account + + const newHatId = predictHatId({ + adminHatId: hatsTree.adminHat.id, + hatsCount: hatsTree.roleHatsTotalCount + newHatCount, }); - for (const stream of fundsToClaimStreams) { - if (!stream.streamId || !stream.contractAddress) { - throw new Error( - 'Stream ID and Stream ContractAddress is required for flush stream transaction', - ); - } - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - stream.streamId, - stream.contractAddress, - getAddress(formHat.wearer), - ); - allTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: formHat.smartAddress, + newHatCount++; + + allTxs.push(await createHatTx(formHat, adminHatId, topHatAccount)); + allTxs.push(mintHatTx(newHatId, formHat)); + allTxs.push(createSmartAccountTx(BigInt(newHatId))); + + // - does it have any streams? + const newStreams = + !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); + if (!!newStreams && newStreams.length > 0) { + const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); + const preparedNewStreams = newStreams.map(stream => { + if ( + !stream.asset || + !stream.startDate || + !stream.endDate || + !stream.amount?.bigintValue || + stream.amount.bigintValue <= 0n + ) { + throw new Error('Form Values inValid', { + cause: stream, + }); + } + + return { + recipient: newPredictedHatSmartAccount, + startDateTs: Math.floor(stream.startDate.getTime() / 1000), + endDateTs: Math.ceil(stream.endDate.getTime() / 1000), + cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), + totalAmount: stream.amount.bigintValue, + assetAddress: stream.asset.address, + }; }); + + // - allTxs.push(create new streams transactions datas) + const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); + allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); + allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); + } + } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { + // Deleted Role + if (formHat.wearer === undefined || formHat.smartAddress === undefined) { + throw new Error('Cannot prepare transactions for removed role without wearer'); } - } - // - does it have any active streams? - const streamsToCancel = formHat?.payments?.filter( - payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), - ); - if (!!streamsToCancel && streamsToCancel.length) { - // - allTxs.push(cancel stream transactions data) - for (const stream of streamsToCancel) { - if (!stream.streamId || !stream.contractAddress) { - throw new Error( - 'Stream ID and Stream ContractAddress is required for cancel stream transaction', + // - does it have any inactive streams which have funds to claim? + const fundsToClaimStreams = formHat?.payments?.filter( + payment => (payment?.withdrawableAmount ?? 0n) > 0n, + ); + if (fundsToClaimStreams && fundsToClaimStreams.length) { + // - allTxs.push(flush stream transaction data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); + for (const stream of fundsToClaimStreams) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + getAddress(formHat.wearer), ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); } - allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); } - } - if ( - (streamsToCancel && streamsToCancel.length) || - (fundsToClaimStreams && fundsToClaimStreams.length) - ) { + + // - does it have any active streams? + const streamsToCancel = formHat?.payments?.filter( + payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), + ); + if (!!streamsToCancel && streamsToCancel.length) { + // - allTxs.push(cancel stream transactions data) + for (const stream of streamsToCancel) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for cancel stream transaction', + ); + } + allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); + } + } + if ( + (streamsToCancel && streamsToCancel.length) || + (fundsToClaimStreams && fundsToClaimStreams.length) + ) { + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } + + // - allTxs.push(deactivate role transaction data) allTxs.push({ calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + abi: HatsAccount1ofNAbi, + functionName: 'execute', + args: [ + hatsProtocol, + 0n, + encodeFunctionData({ + abi: HatsAbi, + functionName: 'setHatStatus', + args: [BigInt(formHat.id), false], + }), + 0, + ], }), - targetAddress: hatsProtocol, + targetAddress: topHatAccount, }); - } + } else { + // Edited Role (existing role) - includes status === Updated + // - else + if ( + formHat.editedRole.status === EditBadgeStatus.Updated && + (formHat.editedRole.fieldNames.includes('roleName') || + formHat.editedRole.fieldNames.includes('roleDescription')) + ) { + if (formHat.name === undefined || formHat.description === undefined) { + throw new Error('Hat name or description of existing hat is undefined.'); + } + const details = await uploadHatDescription( + hatsDetailsBuilder({ + name: formHat.name, + description: formHat.description, + }), + ); + // - is the name or description changed? + // - allTxs.push(edit details data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'changeHatDetails', + args: [BigInt(formHat.id), details], + }), + targetAddress: hatsProtocol, + }); + } + if ( + formHat.editedRole.status === EditBadgeStatus.Updated && + formHat.editedRole.fieldNames.includes('member') + ) { + if (formHat.wearer === undefined || formHat.smartAddress === undefined) { + throw new Error('Cannot prepare transactions for edited role without wearer'); + } + const originalHat = getHat(formHat.id); + if (!originalHat) { + throw new Error('Cannot find original hat'); + } + // - is the member changed? + // - does it have any inactive streams which have funds to claim? + // - allTxs.push(flush stream transaction data) + // - for each active stream + // - if stream was edited, too + // - skip + // - else + // - allTxs.push(flush stream transaction data) + const fundsToClaimStreams = formHat?.payments?.filter( + payment => + (payment?.withdrawableAmount ?? 0n) > 0n && + !!payment.endDate && + payment.endDate > new Date(), + ); - // - allTxs.push(deactivate role transaction data) - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAccount1ofNAbi, - functionName: 'execute', - args: [ - hatsProtocol, - 0n, - encodeFunctionData({ + if (fundsToClaimStreams && fundsToClaimStreams.length) { + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), originalHat.wearer, daoAddress], + }), + targetAddress: hatsProtocol, + }); + for (const stream of fundsToClaimStreams) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + originalHat.wearer, + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); + } + } + // transfer hat to new wearer + allTxs.push({ + calldata: encodeFunctionData({ abi: HatsAbi, - functionName: 'setHatStatus', - args: [BigInt(formHat.id), false], + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], }), - 0, - ], - }), - targetAddress: topHatAccount, - }); - } else { - // Edited Role (existing role) - // - else - // - is the name or description changed? - // - allTxs.push(edit details data) - // - is the member changed? - // - does it have any inactive streams which have funds to claim? - // - allTxs.push(flush stream transaction data) - // - for each active stream - // - if stream was edited, too - // - skip - // - else - // - allTxs.push(flush stream transaction data) - // - for each active streams - // - if stream was edited - // - allTxs.push(flush and cancel stream transaction data) - // - allTxs.push(create new stream transaction data) - // - for each new streams - // - allTxs.push(create new stream transaction data) + targetAddress: hatsProtocol, + }); + } + if ( + formHat.editedRole.status === EditBadgeStatus.Updated && + formHat.editedRole.fieldNames.includes('payments') + ) { + // - for each edited active streams + const editedStreams = formHat?.payments?.filter(payment => { + if (payment.streamId === undefined) { + return false; + } + // @note remove payments that haven't been edited + const originalPayment = getPayment(formHat.id, payment.streamId); + if (originalPayment === null) { + return false; + } + return !isEqual(payment, originalPayment); + }); + if (editedStreams && editedStreams.length) { + if (!formHat.smartAddress || !formHat.wearer) { + throw new Error('Cannot prepare transactions for edited role without wearer'); + } + // - if stream was edited + // - allTxs.push(flush and cancel stream transaction data) + // - allTxs.push(create new stream transaction data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); + for (const stream of editedStreams) { + if ( + !stream.streamId || + !stream.contractAddress || + !stream.amount || + !stream.asset || + !stream.startDate || + !stream.endDate || + !stream.amount.bigintValue + ) { + throw new Error('Cannot prepare transaction; stream data is missing'); + } + const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( + stream.streamId, + stream.contractAddress, + getAddress(formHat.wearer), + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); + allTxs.push(cancelStreamTx); + const preparedNewStream = { + recipient: formHat.smartAddress, + startDateTs: Math.floor(stream.startDate.getTime() / 1000), + endDateTs: Math.ceil(stream.endDate.getTime() / 1000), + cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), + totalAmount: stream.amount.bigintValue, + assetAddress: stream.asset.address, + }; + const newStreamTxData = prepareBatchLinearStreamCreation([preparedNewStream]); + allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); + allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); + } + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } + // - for each new streams + const newStreams = formHat?.payments?.filter(payment => !payment.streamId); + if (newStreams && newStreams.length) { + if (!formHat.smartAddress || !formHat.wearer) { + throw new Error('Cannot prepare transactions, missing data for new streams'); + } + const newPredictedHatSmartAccount = await predictSmartAccount(BigInt(formHat.id)); + const preparedNewStreams = newStreams.map(stream => { + if ( + !stream.asset || + !stream.startDate || + !stream.endDate || + !stream.amount?.bigintValue || + stream.amount.bigintValue <= 0n + ) { + throw new Error('Form Values inValid', { + cause: stream, + }); + } + + return { + recipient: newPredictedHatSmartAccount, + startDateTs: Math.floor(stream.startDate.getTime() / 1000), + endDateTs: Math.ceil(stream.endDate.getTime() / 1000), + cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), + totalAmount: stream.amount.bigintValue, + assetAddress: stream.asset.address, + }; + }); + + // - allTxs.push(create new streams transactions datas) + const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); + allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); + allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); + } + } + } } - } - return allTxs; - }, []); + return allTxs; + }, + [ + daoAddress, + hatsProtocol, + predictSmartAccount, + prepareBatchLinearStreamCreation, + prepareCancelStreamTx, + prepareHatFlushAndCancelPayment, + uploadHatDescription, + createHatTx, + createSmartAccountTx, + getHat, + getPayment, + hatsTree, + mintHatTx, + prepareHatsAccountFlushExecData, + ], + ); const createEditRolesProposal = useCallback( async (values: RoleFormValues, formikHelpers: FormikHelpers) => { From 9b70fcccfc827ded9acd23ebe3b950ac5c0f4e7b Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:54:54 -0400 Subject: [PATCH 12/58] fix stream txs logic --- src/hooks/utils/useCreateRoles.ts | 191 ++++++++++++++++++++++-------- 1 file changed, 141 insertions(+), 50 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 2e3af339b..061120b0d 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -1083,21 +1083,20 @@ export default function useCreateRoles() { throw new Error('Cannot find original hat'); } // - is the member changed? - // - does it have any inactive streams which have funds to claim? - // - allTxs.push(flush stream transaction data) + // - for each inactive streams which have funds to claim? + // - allTxs.push(flush stream transaction data) // - for each active stream // - if stream was edited, too // - skip // - else // - allTxs.push(flush stream transaction data) - const fundsToClaimStreams = formHat?.payments?.filter( + const inactiveFundsToClaimStream = formHat?.payments?.filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n && - !!payment.endDate && - payment.endDate > new Date(), + ((!!payment.endDate && payment.endDate > new Date()) || !!payment.isCancelled), ); - if (fundsToClaimStreams && fundsToClaimStreams.length) { + if (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -1106,7 +1105,7 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - for (const stream of fundsToClaimStreams) { + for (const stream of inactiveFundsToClaimStream) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for flush stream transaction', @@ -1123,39 +1122,125 @@ export default function useCreateRoles() { }); } } - // transfer hat to new wearer - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], - }), - targetAddress: hatsProtocol, + + const unEditedActiveStreams = formHat?.payments?.filter(payment => { + if (payment.streamId === undefined || payment.endDate === undefined) { + return false; + } + // @note remove payments that haven't been edited + const originalPayment = getPayment(formHat.id, payment.streamId); + if (originalPayment === null) { + return false; + } + return ( + isEqual(payment, originalPayment) && + !payment.isCancelled && + payment.endDate < new Date() + ); }); + + if (unEditedActiveStreams && unEditedActiveStreams.length) { + for (const stream of unEditedActiveStreams) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + originalHat.wearer, + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); + } + } + // transfer hat to new wearer after flushing streams + if ((inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || (unEditedActiveStreams && unEditedActiveStreams.length)) { + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } else { + // transfer hat to new wearer without flushing streams + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), originalHat.wearer, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } } + if ( formHat.editedRole.status === EditBadgeStatus.Updated && formHat.editedRole.fieldNames.includes('payments') ) { + if (!formHat.wearer || !formHat.smartAddress) { + throw new Error('Cannot prepare transactions'); + } // - for each edited active streams const editedStreams = formHat?.payments?.filter(payment => { if (payment.streamId === undefined) { return false; } - // @note remove payments that haven't been edited const originalPayment = getPayment(formHat.id, payment.streamId); if (originalPayment === null) { return false; } - return !isEqual(payment, originalPayment); + return ( + !isEqual(payment, originalPayment) && + !payment.isCancelled && + payment.endDate && + payment.endDate > new Date() + ); }); - if (editedStreams && editedStreams.length) { - if (!formHat.smartAddress || !formHat.wearer) { - throw new Error('Cannot prepare transactions for edited role without wearer'); + const editedStreamsWithFundsToClaim = editedStreams?.filter( + stream => (stream?.withdrawableAmount ?? 0n) > 0n, + ); + if (editedStreamsWithFundsToClaim && editedStreamsWithFundsToClaim.length) { + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); + for (const stream of editedStreamsWithFundsToClaim) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + getAddress(formHat.wearer), + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); } - // - if stream was edited - // - allTxs.push(flush and cancel stream transaction data) - // - allTxs.push(create new stream transaction data) + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + } + if (editedStreams && editedStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -1164,6 +1249,30 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); + for (const stream of editedStreams) { + if (!stream.streamId || !stream.contractAddress) { + throw new Error( + 'Stream ID and Stream ContractAddress is required for flush stream transaction', + ); + } + const cancelStreamTx = prepareCancelStreamTx( + stream.streamId, + stream.contractAddress, + ); + allTxs.push(cancelStreamTx); + } + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], + }), + targetAddress: hatsProtocol, + }); + // - if stream was edited + // - allTxs.push(flush and cancel stream transaction data) + // - allTxs.push(create new stream transaction data) + for (const stream of editedStreams) { if ( !stream.streamId || @@ -1176,16 +1285,7 @@ export default function useCreateRoles() { ) { throw new Error('Cannot prepare transaction; stream data is missing'); } - const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( - stream.streamId, - stream.contractAddress, - getAddress(formHat.wearer), - ); - allTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: formHat.smartAddress, - }); - allTxs.push(cancelStreamTx); + const preparedNewStream = { recipient: formHat.smartAddress, startDateTs: Math.floor(stream.startDate.getTime() / 1000), @@ -1198,14 +1298,6 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], - }), - targetAddress: hatsProtocol, - }); } // - for each new streams const newStreams = formHat?.payments?.filter(payment => !payment.streamId); @@ -1254,7 +1346,6 @@ export default function useCreateRoles() { predictSmartAccount, prepareBatchLinearStreamCreation, prepareCancelStreamTx, - prepareHatFlushAndCancelPayment, uploadHatDescription, createHatTx, createSmartAccountTx, @@ -1373,14 +1464,14 @@ export default function useCreateRoles() { } // // All done, submit the proposal! - // await submitProposal({ - // proposalData, - // nonce: values.customNonce ?? safe.nextNonce, - // pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), - // successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), - // failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - // successCallback: submitProposalSuccessCallback, - // }); + await submitProposal({ + proposalData, + nonce: values.customNonce ?? safe.nextNonce, + pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), + successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), + failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), + successCallback: submitProposalSuccessCallback, + }); } catch (e) { console.error(e); toast(t('encodingFailedMessage', { ns: 'proposal' })); From 93024cc84072feaf43531bab7c4b9276d028381b Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:57:46 -0400 Subject: [PATCH 13/58] remove commented code --- src/hooks/utils/useCreateRoles.ts | 636 +----------------------------- 1 file changed, 16 insertions(+), 620 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 061120b0d..12d95d854 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -12,17 +12,10 @@ import GnosisSafeL2 from '../../assets/abi/GnosisSafeL2'; import { HatsAbi } from '../../assets/abi/HatsAbi'; import HatsAccount1ofNAbi from '../../assets/abi/HatsAccount1ofN'; import { - // AddedHatsWithIds, EditBadgeStatus, HatStruct, - // PreparedAddedHatsData, - // PreparedChangedRoleDetailsData, - // PreparedMemberChangeData, - // PreparedNewStreamData, - // PreparedEditedStreamData, RoleFormValues, RoleHatFormValueEdited, - // SablierPayment, } from '../../components/pages/Roles/types'; import { DAO_ROUTES } from '../../constants/routes'; import { useFractal } from '../../providers/App/AppProvider'; @@ -95,229 +88,6 @@ const createHatStructsFromRolesFormValues = async ( ); }; -// const identifyAndPrepareRemovedHats = (modifiedHats: RoleHatFormValueEdited[]) => { -// return modifiedHats -// .filter(hat => hat.editedRole.status === EditBadgeStatus.Removed) -// .map(hat => { -// if (hat.id === undefined) { -// throw new Error('Hat ID of removed hat is undefined.'); -// } - -// return hat.id; -// }); -// }; - -// const identifyAndPrepareMemberChangedHats = ( -// modifiedHats: RoleHatFormValueEdited[], -// getHat: (hatId: Hex) => DecentRoleHat | null, -// ) => { -// return modifiedHats -// .filter( -// hat => -// hat.editedRole.status === EditBadgeStatus.Updated && -// hat.editedRole.fieldNames.includes('member'), -// ) -// .map(hat => { -// if (hat.wearer === undefined || hat.id === undefined) { -// throw new Error('Hat wearer of member changed Hat is undefined.'); -// } - -// const currentHat = getHat(hat.id); -// if (currentHat === null) { -// throw new Error("Couldn't find existing Hat for member changed Hat."); -// } - -// const hatWearerChanged = { -// id: currentHat.id, -// currentWearer: getAddress(currentHat.wearer), -// newWearer: getAddress(hat.wearer), -// }; - -// return hatWearerChanged; -// }) -// .filter(hat => hat.currentWearer !== hat.newWearer); -// }; - -// const identifyAndPrepareRoleDetailsChangedHats = ( -// modifiedHats: RoleHatFormValueEdited[], -// uploadHatDescription: (hatDescription: string) => Promise, -// getHat: (hatId: Hex) => DecentRoleHat | null, -// ) => { -// return Promise.all( -// modifiedHats -// .filter( -// formHat => -// formHat.editedRole.status === EditBadgeStatus.Updated && -// (formHat.editedRole.fieldNames.includes('roleName') || -// formHat.editedRole.fieldNames.includes('roleDescription')), -// ) -// .map(async formHat => { -// if (formHat.id === undefined) { -// throw new Error('Hat ID of existing hat is undefined.'); -// } - -// if (formHat.name === undefined || formHat.description === undefined) { -// throw new Error('Hat name or description of existing hat is undefined.'); -// } - -// const currentHat = getHat(formHat.id); -// if (currentHat === null) { -// throw new Error("Couldn't find existing Hat for details changed Hat."); -// } - -// return { -// id: currentHat.id, -// details: await uploadHatDescription( -// hatsDetailsBuilder({ -// name: formHat.name, -// description: formHat.description, -// }), -// ), -// }; -// }), -// ); -// }; - -// const identifyAndPrepareEditedPaymentStreams = ( -// modifiedHats: RoleHatFormValueEdited[], -// getHat: (hatId: Hex) => DecentRoleHat | null, -// getPayment: (hatId: Hex, streamId: string) => SablierPayment | null, -// ): PreparedEditedStreamData[] => { -// return modifiedHats.flatMap(formHat => { -// const currentHat = getHat(formHat.id); -// if (currentHat === null) { -// return []; -// } - -// if (formHat.payments === undefined) { -// return []; -// } - -// return formHat.payments -// .filter(payment => { -// if (payment.streamId === undefined) { -// return false; -// } -// // @note remove payments that haven't been edited -// const originalPayment = getPayment(formHat.id, payment.streamId); -// if (originalPayment === null) { -// return false; -// } -// return !isEqual(payment, originalPayment); -// }) -// .map(payment => { -// if ( -// !payment.streamId || -// !payment.contractAddress || -// !payment.asset || -// !payment.startDate || -// !payment.endDate || -// !payment.amount?.bigintValue || -// payment.amount.bigintValue <= 0n -// ) { -// throw new Error('Form Values inValid', { -// cause: payment, -// }); -// } - -// return { -// streamId: payment.streamId, -// recipient: currentHat.smartAddress, -// startDateTs: Math.floor(payment.startDate.getTime() / 1000), -// endDateTs: Math.ceil(payment.endDate.getTime() / 1000), -// cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), -// totalAmount: payment.amount.bigintValue, -// assetAddress: payment.asset.address, -// roleHatId: BigInt(currentHat.id), -// roleHatWearer: currentHat.wearer, -// roleHatSmartAddress: currentHat.smartAddress, -// streamContractAddress: payment.contractAddress, -// }; -// }); -// }); -// }; - -// const identifyAndPrepareAddedPaymentStreams = async ( -// modifiedHats: RoleHatFormValueEdited[], -// addedHatsWithIds: AddedHatsWithIds[], -// getHat: (hatId: Hex) => DecentRoleHat | null, -// predictSmartAccount: (hatId: bigint) => Promise
, -// ): Promise => { -// const preparedStreamDataMapped = await Promise.all( -// modifiedHats.map(async formHat => { -// if (formHat.payments === undefined) { -// return []; -// } - -// const payments = formHat.payments.filter(payment => !payment.streamId); -// let recipientAddress: Address; -// const existingRoleHat = getHat(formHat.id); -// if (!!existingRoleHat) { -// recipientAddress = existingRoleHat.smartAddress; -// } else { -// const addedRoleHat = addedHatsWithIds.find(addedHat => addedHat.formId === formHat.id); -// if (!addedRoleHat) { -// throw new Error('Could not find added role hat for added payment stream.'); -// } -// recipientAddress = await predictSmartAccount(addedRoleHat.id); -// } -// return payments.map(payment => { -// if ( -// !payment.asset || -// !payment.startDate || -// !payment.endDate || -// !payment.amount?.bigintValue || -// payment.amount.bigintValue <= 0n -// ) { -// throw new Error('Form Values inValid', { -// cause: payment, -// }); -// } - -// return { -// recipient: recipientAddress, -// startDateTs: Math.floor(payment.startDate.getTime() / 1000), -// endDateTs: Math.ceil(payment.endDate.getTime() / 1000), -// cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), -// totalAmount: payment.amount.bigintValue, -// assetAddress: payment.asset.address, -// }; -// }); -// }), -// ); -// return preparedStreamDataMapped.flat(); -// }; - -// const prepareAddHatsTxArgs = (addedHats: HatStruct[], adminHatId: Hex, topHatAccount: Address) => { -// const admins: bigint[] = []; -// const details: string[] = []; -// const maxSupplies: number[] = []; -// const eligibilityModules: Address[] = []; -// const toggleModules: Address[] = []; -// const mutables: boolean[] = []; -// const imageURIs: string[] = []; - -// addedHats.forEach(hat => { -// admins.push(BigInt(adminHatId)); -// details.push(hat.details); -// maxSupplies.push(hat.maxSupply); -// eligibilityModules.push(topHatAccount); -// toggleModules.push(topHatAccount); -// mutables.push(hat.isMutable); -// imageURIs.push(hat.imageURI); -// }); - -// return [ -// admins, -// details, -// maxSupplies, -// eligibilityModules, -// toggleModules, -// mutables, -// imageURIs, -// ] as const; -// }; - export default function useCreateRoles() { const { node: { safe, daoAddress, daoName }, @@ -472,347 +242,6 @@ export default function useCreateRoles() { [prepareFlushStreamTx], ); - const prepareHatFlushAndCancelPayment = useCallback( - (streamId: string, contractAddress: Address, wearer: Address) => { - const cancelStreamTx = prepareCancelStreamTx(streamId, contractAddress); - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - streamId, - contractAddress, - wearer, - ); - - return { wrappedFlushStreamTx, cancelStreamTx }; - }, - [prepareCancelStreamTx, prepareHatsAccountFlushExecData], - ); - - // const prepareEditHatsProposalData = useCallback( - // ( - // proposalMetadata: CreateProposalMetadata, - // addedHats: PreparedAddedHatsData[], - // removedHatIds: Hex[], - // memberChangedHats: PreparedMemberChangeData[], - // roleDetailsChangedHats: PreparedChangedRoleDetailsData[], - // editedPaymentStreams: PreparedEditedStreamData[], - // addedPaymentStreams: PreparedNewStreamData[], - // ) => { - // if (!hatsTree || !daoAddress) { - // throw new Error('Can not edit hats without Hats Tree!'); - // } - - // const topHatAccount = hatsTree.topHat.smartAddress; - // const adminHatId = hatsTree.adminHat.id; - - // const createAndMintHatsTxs: Hex[] = []; - // let removeHatTxs: Hex[] = []; - // let transferHatTxs: Hex[] = []; - // let hatDetailsChangedTxs: Hex[] = []; - - // let smartAccountTxs: { calldata: Hex; targetAddress: Address }[] = []; - // let hatPaymentAddedTxs: { calldata: Hex; targetAddress: Address }[] = []; - // let hatPaymentEditedTxs: { calldata: Hex; targetAddress: Address }[] = []; - - // let hatPaymentWearerChangedTxs: { calldata: Hex; targetAddress: Address }[] = []; - // let hatPaymentHatRemovedTxs: { calldata: Hex; targetAddress: Address }[] = []; - - // // let paymentStreamFlushAndCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; - // // let paymentStreamFlushTxs: { calldata: Hex; targetAddress: Address }[] = []; - // // let paymentStreamCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; - // // @todo should not flush same stream more than once - // // @todo possibly remove duplicate flushes after transactions have been prepared - // if (addedHats.length) { - // // First, prepare a single tx to create all the hats - // const createHatsTx = encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'batchCreateHats', - // args: prepareAddHatsTxArgs(addedHats, adminHatId, topHatAccount), - // }); - - // const mintHatsTx = encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'batchMintHats', - // // @note hatIds[], wearers[] - // args: [addedHats.map(h => h.id), addedHats.map(h => h.wearer)], - // }); - - // // finally, finally create smart account for hats. - // const createSmartAccountCallDatas = addedHats.map(hat => { - // return encodeFunctionData({ - // abi: ERC6551RegistryAbi, - // functionName: 'createAccount', - // args: [ - // hatsAccount1ofNMasterCopy, - // getERC6551RegistrySalt(BigInt(chain.id), getAddress(decentHatsMasterCopy)), - // BigInt(chain.id), - // hatsProtocol, - // hat.id, - // ], - // }); - // }); - // // Push these two txs to the included txs array. - // // They will be executed in order: add all hats first, then mint all hats to their respective wearers. - // createAndMintHatsTxs.push(createHatsTx); - // createAndMintHatsTxs.push(mintHatsTx); - // smartAccountTxs.push( - // ...createSmartAccountCallDatas.map(calldata => ({ - // calldata, - // targetAddress: erc6551Registry, - // })), - // ); - // } - - // if (removedHatIds.length) { - // removedHatIds.forEach(hatId => { - // const roleHat = hatsTree.roleHats.find(hat => hat.id === hatId); - // if (roleHat && roleHat.payments?.length) { - // /** - // * Assumption: current state of blockchain - // * Fact: does the hat currently have funds to withdraw - // * Do: if yes, then flush the stream - // */ - - // const fundsToClaimStreams = roleHat.payments.filter( - // payment => payment.withdrawableAmount > 0n, - // ); - // if (fundsToClaimStreams.length) { - // hatPaymentHatRemovedTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [BigInt(hatId), roleHat.wearer, daoAddress], - // }), - // targetAddress: hatsProtocol, - // }); - // fundsToClaimStreams.forEach(payment => { - // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - // payment.streamId, - // payment.contractAddress, - // roleHat.wearer, - // ); - // hatPaymentHatRemovedTxs.push({ - // calldata: wrappedFlushStreamTx, - // targetAddress: roleHat.smartAddress, - // }); - // }); - // } - // /** - // * Assumption: current state of blockchain - // * Fact: does the hat currently have funds that are not active or have not ended - // * Do: if yes, then cancel the stream - // */ - // const streamsToCancel = roleHat.payments.filter( - // payment => !payment.isCancelled && payment.endDate > new Date(), - // ); - // streamsToCancel.forEach(payment => { - // hatPaymentHatRemovedTxs.push( - // prepareCancelStreamTx(payment.streamId, payment.contractAddress), - // ); - // }); - // if (streamsToCancel.length || fundsToClaimStreams.length) { - // hatPaymentHatRemovedTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [BigInt(hatId), daoAddress, roleHat.wearer], - // }), - // targetAddress: hatsProtocol, - // }); - // } - // } - - // // make transaction proxy through erc6551 contract - // removeHatTxs.push( - // encodeFunctionData({ - // abi: HatsAccount1ofNAbi, - // functionName: 'execute', - // args: [ - // hatsProtocol, - // 0n, - // encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'setHatStatus', - // args: [BigInt(hatId), false], - // }), - // 0, - // ], - // }), - // ); - // }); - // } - - // if (memberChangedHats.length) { - // memberChangedHats.map(({ id, currentWearer, newWearer }) => { - // const roleHat = hatsTree.roleHats.find(hat => hat.id === id); - // if (roleHat && roleHat.payments?.length) { - // /** - // * Assumption: current state of blockchain - // * Fact: does the hat currently have funds to withdraw - // * Do: if yes, then flush the stream - // */ - // const fundsToClaimStreams = roleHat.payments.filter( - // payment => payment.withdrawableAmount > 0n, - // ); - // if (fundsToClaimStreams.length) { - // hatPaymentWearerChangedTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [BigInt(id), currentWearer, daoAddress], - // }), - // targetAddress: hatsProtocol, - // }); - // fundsToClaimStreams.forEach(payment => { - // const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - // payment.streamId, - // payment.contractAddress, - // roleHat.wearer, - // ); - // hatPaymentWearerChangedTxs.push({ - // calldata: wrappedFlushStreamTx, - // targetAddress: roleHat.smartAddress, - // }); - // }); - // hatPaymentWearerChangedTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [BigInt(id), daoAddress, newWearer], - // }), - // targetAddress: hatsProtocol, - // }); - // } - // } else { - // transferHatTxs.push( - // encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [BigInt(id), daoAddress, newWearer], - // }), - // ); - // } - // }); - // } - - // if (roleDetailsChangedHats.length) { - // hatDetailsChangedTxs = roleDetailsChangedHats.map(({ id, details }) => { - // return encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'changeHatDetails', - // args: [BigInt(id), details], - // }); - // }); - // } - - // if (addedPaymentStreams.length) { - // const preparedPaymentTransactions = prepareBatchLinearStreamCreation(addedPaymentStreams); - // hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); - // hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); - // } - - // if (editedPaymentStreams.length) { - // /** - // * Assumption: current state of blockchain - // * Fact: does the hat currently have funds to withdraw - // * Do: if yes, then flush the stream - // */ - // const paymentCancelTxs: { calldata: Hex; targetAddress: Address }[] = []; - // editedPaymentStreams.forEach(paymentStream => { - // paymentCancelTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [paymentStream.roleHatId, paymentStream.roleHatWearer, daoAddress], - // }), - // targetAddress: hatsProtocol, - // }); - // const { wrappedFlushStreamTx, cancelStreamTx } = prepareHatFlushAndCancelPayment( - // paymentStream.streamId, - // paymentStream.streamContractAddress, - // paymentStream.roleHatWearer, - // ); - // paymentCancelTxs.push({ - // calldata: wrappedFlushStreamTx, - // targetAddress: paymentStream.roleHatSmartAddress, - // }); - // paymentCancelTxs.push(cancelStreamTx); - // paymentCancelTxs.push({ - // calldata: encodeFunctionData({ - // abi: HatsAbi, - // functionName: 'transferHat', - // args: [paymentStream.roleHatId, daoAddress, paymentStream.roleHatWearer], - // }), - // targetAddress: hatsProtocol, - // }); - // }); - - // const preparedPaymentTransactions = prepareBatchLinearStreamCreation(editedPaymentStreams); - // hatPaymentEditedTxs.push(...paymentCancelTxs); - // hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); - // hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); - // } - // console.log('🚀 ~ createAndMintHatsTxs:', createAndMintHatsTxs); - // console.log('🚀 ~ transferHatTxs:', transferHatTxs); - // console.log('🚀 ~ ...hatPaymentWearerChangedTxs.map:', hatPaymentWearerChangedTxs); - // console.log('🚀 ~ hatDetailsChangedTxs:', hatDetailsChangedTxs); - // console.log('🚀 ~ hatPaymentAddedTxs:', hatPaymentAddedTxs); - // console.log('🚀 ~ hatPaymentEditedTxs:', hatPaymentEditedTxs); - // console.log('🚀 ~ smartAccountTxs:', smartAccountTxs); - // console.log('🚀 ~ removeHatTxs:', removeHatTxs); - // console.log('🚀 ~ hatPaymentHatRemovedTxs:', hatPaymentHatRemovedTxs); - // const proposalTransactions = { - // targets: [ - // ...createAndMintHatsTxs.map(() => hatsProtocol), - // ...smartAccountTxs.map(({ targetAddress }) => targetAddress), - // ...hatPaymentHatRemovedTxs.map(({ targetAddress }) => targetAddress), - // ...removeHatTxs.map(() => topHatAccount), - // ...hatPaymentWearerChangedTxs.map(({ targetAddress }) => targetAddress), - // ...transferHatTxs.map(() => hatsProtocol), - // ...hatDetailsChangedTxs.map(() => hatsProtocol), - // ...hatPaymentAddedTxs.map(({ targetAddress }) => targetAddress), - // ...hatPaymentEditedTxs.map(({ targetAddress }) => targetAddress), - // ], - // calldatas: [ - // ...createAndMintHatsTxs, - // ...smartAccountTxs.map(({ calldata }) => calldata), - // ...hatPaymentHatRemovedTxs.map(({ calldata }) => calldata), - // ...removeHatTxs, - // ...hatPaymentWearerChangedTxs.map(({ calldata }) => calldata), - // ...transferHatTxs, - // ...hatDetailsChangedTxs, - // ...hatPaymentAddedTxs.map(({ calldata }) => calldata), - // ...hatPaymentEditedTxs.map(({ calldata }) => calldata), - // ], - // metaData: proposalMetadata, - // values: [ - // ...createAndMintHatsTxs.map(() => 0n), - // ...smartAccountTxs.map(() => 0n), - // ...hatPaymentHatRemovedTxs.map(() => 0n), - // ...removeHatTxs.map(() => 0n), - // ...hatPaymentWearerChangedTxs.map(() => 0n), - // ...transferHatTxs.map(() => 0n), - // ...hatDetailsChangedTxs.map(() => 0n), - // ...hatPaymentAddedTxs.map(() => 0n), - // ...hatPaymentEditedTxs.map(() => 0n), - // ], - // }; - - // return proposalTransactions; - // }, - // [ - // hatsProtocol, - // hatsTree, - // prepareCancelStreamTx, - // prepareHatFlushAndCancelPayment, - // prepareHatsAccountFlushExecData, - // daoAddress, - // prepareBatchLinearStreamCreation, - // hatsAccount1ofNMasterCopy, - // chain.id, - // decentHatsMasterCopy, - // erc6551Registry, - // ], - // ); - const createHatTx = useCallback( async (formRole: RoleHatFormValueEdited, adminHatId: bigint, topHatSmartAccount: Address) => { if (formRole.name === undefined || formRole.description === undefined) { @@ -1127,7 +556,6 @@ export default function useCreateRoles() { if (payment.streamId === undefined || payment.endDate === undefined) { return false; } - // @note remove payments that haven't been edited const originalPayment = getPayment(formHat.id, payment.streamId); if (originalPayment === null) { return false; @@ -1158,7 +586,10 @@ export default function useCreateRoles() { } } // transfer hat to new wearer after flushing streams - if ((inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || (unEditedActiveStreams && unEditedActiveStreams.length)) { + if ( + (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || + (unEditedActiveStreams && unEditedActiveStreams.length) + ) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -1409,52 +840,6 @@ export default function useCreateRoles() { const allTxs = await prepareAllTxs(modifiedHats); console.log('🚀 ~ allTxs:', allTxs); - // Convert addedHats to include predicted id - // const addedHatsWithIds = await Promise.all( - // addedHatsRolesValues.map(async (hat, index) => { - // const hatId = predictHatId({ - // adminHatId: hatsTree.adminHat.id, - // hatsCount: hatsTree.roleHatsTotalCount + index, - // }); - // return { - // ...hat, - // id: hatId, - // // @note For new hats, a randomId is created for temporary indentification - // formId: hat.id, - // }; - // }), - // ); - - // const removedHatIds = identifyAndPrepareRemovedHats(modifiedHats); - // const memberChangedHats = identifyAndPrepareMemberChangedHats(modifiedHats, getHat); - // const roleDetailsChangedHats = await identifyAndPrepareRoleDetailsChangedHats( - // modifiedHats, - // uploadHatDescription, - // getHat, - // ); - // const editedPaymentStreams = identifyAndPrepareEditedPaymentStreams( - // modifiedHats, - // getHat, - // getPayment, - // ); - - // const addedPaymentStreamsOnNewHats = await identifyAndPrepareAddedPaymentStreams( - // modifiedHats, - // addedHatsWithIds, - // getHat, - // predictSmartAccount, - // ); - - // proposalData = prepareEditHatsProposalData( - // values.proposalMetadata, - // addedHats.map((hat, index) => ({ ...hat, id: addedHatsWithIds[index].id })), - // removedHatIds, - // memberChangedHats, - // roleDetailsChangedHats, - // editedPaymentStreams, - // addedPaymentStreamsOnNewHats, - // ); - proposalData = { targets: allTxs.map(({ targetAddress }) => targetAddress), calldatas: allTxs.map(({ calldata }) => calldata), @@ -1479,7 +864,18 @@ export default function useCreateRoles() { formikHelpers.setSubmitting(false); } }, - [], + [ + safe, + hatsTreeId, + hatsTree, + submitProposal, + prepareCreateTopHatProposalData, + prepareAllTxs, + t, + submitProposalSuccessCallback, + uploadHatDescription, + publicClient, + ], ); return { From 343c98c80cb69160bb0991f45cd91d8d671c9cb9 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:54:27 -0400 Subject: [PATCH 14/58] update comments --- src/hooks/utils/useCreateRoles.ts | 57 +++++++++++++------------------ 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 12d95d854..fbb0784a3 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -335,15 +335,14 @@ export default function useCreateRoles() { // "active stream" = not cancelled and not past end date // "inactive stream" = cancelled or past end date - // for each modified role for (let index = 0; index < modifiedHats.length; index++) { const formHat = modifiedHats[index]; if (formHat.editedRole.status === EditBadgeStatus.New) { - // New Role - // - "create new role" transaction data - // - includes create hat, mint hat, create smart account - + /** + * New Role + * Create new hat, mint it, create smart account, and create new streams + */ const newHatId = predictHatId({ adminHatId: hatsTree.adminHat.id, hatsCount: hatsTree.roleHatsTotalCount + newHatCount, @@ -354,7 +353,6 @@ export default function useCreateRoles() { allTxs.push(mintHatTx(newHatId, formHat)); allTxs.push(createSmartAccountTx(BigInt(newHatId))); - // - does it have any streams? const newStreams = !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); if (!!newStreams && newStreams.length > 0) { @@ -382,23 +380,23 @@ export default function useCreateRoles() { }; }); - // - allTxs.push(create new streams transactions datas) const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { - // Deleted Role + /** + * Removed Role + * Transfer hat to DAO, flush streams, cancel streams, transfer hat to back to wearer, set hat status to false + */ if (formHat.wearer === undefined || formHat.smartAddress === undefined) { throw new Error('Cannot prepare transactions for removed role without wearer'); } - // - does it have any inactive streams which have funds to claim? const fundsToClaimStreams = formHat?.payments?.filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n, ); if (fundsToClaimStreams && fundsToClaimStreams.length) { - // - allTxs.push(flush stream transaction data) allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -425,12 +423,10 @@ export default function useCreateRoles() { } } - // - does it have any active streams? const streamsToCancel = formHat?.payments?.filter( payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), ); if (!!streamsToCancel && streamsToCancel.length) { - // - allTxs.push(cancel stream transactions data) for (const stream of streamsToCancel) { if (!stream.streamId || !stream.contractAddress) { throw new Error( @@ -454,7 +450,6 @@ export default function useCreateRoles() { }); } - // - allTxs.push(deactivate role transaction data) allTxs.push({ calldata: encodeFunctionData({ abi: HatsAccount1ofNAbi, @@ -473,13 +468,15 @@ export default function useCreateRoles() { targetAddress: topHatAccount, }); } else { - // Edited Role (existing role) - includes status === Updated - // - else if ( formHat.editedRole.status === EditBadgeStatus.Updated && (formHat.editedRole.fieldNames.includes('roleName') || formHat.editedRole.fieldNames.includes('roleDescription')) ) { + /** + * Updated Role Name or Description Transaction + * Upload the new details to IPFS, Change hat details + */ if (formHat.name === undefined || formHat.description === undefined) { throw new Error('Hat name or description of existing hat is undefined.'); } @@ -489,8 +486,6 @@ export default function useCreateRoles() { description: formHat.description, }), ); - // - is the name or description changed? - // - allTxs.push(edit details data) allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -504,6 +499,11 @@ export default function useCreateRoles() { formHat.editedRole.status === EditBadgeStatus.Updated && formHat.editedRole.fieldNames.includes('member') ) { + /** + * Updated Role Member + * Transfer hat to DAO, flush inactive and unedited streams, transfer hat to new wearer + */ + if (formHat.wearer === undefined || formHat.smartAddress === undefined) { throw new Error('Cannot prepare transactions for edited role without wearer'); } @@ -511,14 +511,6 @@ export default function useCreateRoles() { if (!originalHat) { throw new Error('Cannot find original hat'); } - // - is the member changed? - // - for each inactive streams which have funds to claim? - // - allTxs.push(flush stream transaction data) - // - for each active stream - // - if stream was edited, too - // - skip - // - else - // - allTxs.push(flush stream transaction data) const inactiveFundsToClaimStream = formHat?.payments?.filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n && @@ -585,7 +577,7 @@ export default function useCreateRoles() { }); } } - // transfer hat to new wearer after flushing streams + // transfer hat from DAO to new wearer if there are any funds to claim if ( (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || (unEditedActiveStreams && unEditedActiveStreams.length) @@ -599,7 +591,7 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); } else { - // transfer hat to new wearer without flushing streams + // transfer hat from original wearer to new wearer if there are no funds to claim allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -615,10 +607,14 @@ export default function useCreateRoles() { formHat.editedRole.status === EditBadgeStatus.Updated && formHat.editedRole.fieldNames.includes('payments') ) { + /** + * Updated Role Payments + * Transfer hat to DAO, flush edited active streams, cancel streams, transfer hat to back to wearer, create new streams + */ + if (!formHat.wearer || !formHat.smartAddress) { throw new Error('Cannot prepare transactions'); } - // - for each edited active streams const editedStreams = formHat?.payments?.filter(payment => { if (payment.streamId === undefined) { return false; @@ -700,9 +696,6 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - // - if stream was edited - // - allTxs.push(flush and cancel stream transaction data) - // - allTxs.push(create new stream transaction data) for (const stream of editedStreams) { if ( @@ -730,7 +723,6 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } } - // - for each new streams const newStreams = formHat?.payments?.filter(payment => !payment.streamId); if (newStreams && newStreams.length) { if (!formHat.smartAddress || !formHat.wearer) { @@ -760,7 +752,6 @@ export default function useCreateRoles() { }; }); - // - allTxs.push(create new streams transactions datas) const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); From 49b1de159199f7132fa531dd988b31b6ecda7e87 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 23:34:59 -0400 Subject: [PATCH 15/58] reuse redudant code --- src/hooks/utils/useCreateRoles.ts | 124 ++++++++++++------------------ 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index fbb0784a3..56b17d1b0 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -16,6 +16,7 @@ import { HatStruct, RoleFormValues, RoleHatFormValueEdited, + SablierPaymentFormValues, } from '../../components/pages/Roles/types'; import { DAO_ROUTES } from '../../constants/routes'; import { useFractal } from '../../providers/App/AppProvider'; @@ -317,6 +318,36 @@ export default function useCreateRoles() { [chain.id, decentHatsMasterCopy, erc6551Registry, hatsAccount1ofNMasterCopy, hatsProtocol], ); + const createBatchLinearStreamCreationTx = useCallback( + (formStreams: SablierPaymentFormValues[], roleSmartAccountAddress: Address) => { + const preparedStreams = formStreams.map(stream => { + if ( + !stream.asset || + !stream.startDate || + !stream.endDate || + !stream.amount?.bigintValue || + stream.amount.bigintValue <= 0n + ) { + throw new Error('Form Values inValid', { + cause: stream, + }); + } + + return { + recipient: roleSmartAccountAddress, + startDateTs: Math.floor(stream.startDate.getTime() / 1000), + endDateTs: Math.ceil(stream.endDate.getTime() / 1000), + cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), + totalAmount: stream.amount.bigintValue, + assetAddress: stream.asset.address, + }; + }); + + return prepareBatchLinearStreamCreation(preparedStreams); + }, + [prepareBatchLinearStreamCreation], + ); + const prepareAllTxs = useCallback( async (modifiedHats: RoleHatFormValueEdited[]) => { if (!hatsTree || !daoAddress) { @@ -357,30 +388,10 @@ export default function useCreateRoles() { !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); if (!!newStreams && newStreams.length > 0) { const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); - const preparedNewStreams = newStreams.map(stream => { - if ( - !stream.asset || - !stream.startDate || - !stream.endDate || - !stream.amount?.bigintValue || - stream.amount.bigintValue <= 0n - ) { - throw new Error('Form Values inValid', { - cause: stream, - }); - } - - return { - recipient: newPredictedHatSmartAccount, - startDateTs: Math.floor(stream.startDate.getTime() / 1000), - endDateTs: Math.ceil(stream.endDate.getTime() / 1000), - cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: stream.amount.bigintValue, - assetAddress: stream.asset.address, - }; - }); - - const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); + const newStreamTxData = createBatchLinearStreamCreationTx( + newStreams, + newPredictedHatSmartAccount, + ); allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } @@ -697,62 +708,27 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); - for (const stream of editedStreams) { - if ( - !stream.streamId || - !stream.contractAddress || - !stream.amount || - !stream.asset || - !stream.startDate || - !stream.endDate || - !stream.amount.bigintValue - ) { - throw new Error('Cannot prepare transaction; stream data is missing'); - } - - const preparedNewStream = { - recipient: formHat.smartAddress, - startDateTs: Math.floor(stream.startDate.getTime() / 1000), - endDateTs: Math.ceil(stream.endDate.getTime() / 1000), - cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: stream.amount.bigintValue, - assetAddress: stream.asset.address, - }; - const newStreamTxData = prepareBatchLinearStreamCreation([preparedNewStream]); - allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); - allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); - } + const newStreamTxData = createBatchLinearStreamCreationTx( + editedStreams, + formHat.smartAddress, + ); + allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); + allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } + /** + * New Streams + * Create new streams + */ const newStreams = formHat?.payments?.filter(payment => !payment.streamId); if (newStreams && newStreams.length) { if (!formHat.smartAddress || !formHat.wearer) { throw new Error('Cannot prepare transactions, missing data for new streams'); } const newPredictedHatSmartAccount = await predictSmartAccount(BigInt(formHat.id)); - const preparedNewStreams = newStreams.map(stream => { - if ( - !stream.asset || - !stream.startDate || - !stream.endDate || - !stream.amount?.bigintValue || - stream.amount.bigintValue <= 0n - ) { - throw new Error('Form Values inValid', { - cause: stream, - }); - } - - return { - recipient: newPredictedHatSmartAccount, - startDateTs: Math.floor(stream.startDate.getTime() / 1000), - endDateTs: Math.ceil(stream.endDate.getTime() / 1000), - cliffDateTs: Math.floor((stream.cliffDate?.getTime() ?? 0) / 1000), - totalAmount: stream.amount.bigintValue, - assetAddress: stream.asset.address, - }; - }); - - const newStreamTxData = prepareBatchLinearStreamCreation(preparedNewStreams); + const newStreamTxData = createBatchLinearStreamCreationTx( + newStreams, + newPredictedHatSmartAccount, + ); allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } @@ -766,7 +742,6 @@ export default function useCreateRoles() { daoAddress, hatsProtocol, predictSmartAccount, - prepareBatchLinearStreamCreation, prepareCancelStreamTx, uploadHatDescription, createHatTx, @@ -776,6 +751,7 @@ export default function useCreateRoles() { hatsTree, mintHatTx, prepareHatsAccountFlushExecData, + createBatchLinearStreamCreationTx, ], ); From e36e63df17452beffc95cc5519e1789dfda83f54 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Wed, 4 Sep 2024 23:36:14 -0400 Subject: [PATCH 16/58] update comment --- src/hooks/utils/useCreateRoles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 56b17d1b0..3893c5542 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -620,7 +620,7 @@ export default function useCreateRoles() { ) { /** * Updated Role Payments - * Transfer hat to DAO, flush edited active streams, cancel streams, transfer hat to back to wearer, create new streams + * Transfer hat to DAO, flush edited active streams, cancel edited streams, transfer hat to back to wearer, create new streams */ if (!formHat.wearer || !formHat.smartAddress) { From 8807d88c7cea90ea00d47a97bf3778a5b2209661 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:07:22 -0400 Subject: [PATCH 17/58] remove console log --- src/hooks/utils/useCreateRoles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 3893c5542..b7008d53f 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -805,7 +805,6 @@ export default function useCreateRoles() { } const allTxs = await prepareAllTxs(modifiedHats); - console.log('🚀 ~ allTxs:', allTxs); proposalData = { targets: allTxs.map(({ targetAddress }) => targetAddress), From b34526e1e8be4407b41f0d9551ac08229d981c5b Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:06:47 -0400 Subject: [PATCH 18/58] add check for withdrawable funds --- src/hooks/utils/useCreateRoles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index b7008d53f..84705edb5 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -566,7 +566,8 @@ export default function useCreateRoles() { return ( isEqual(payment, originalPayment) && !payment.isCancelled && - payment.endDate < new Date() + payment.endDate < new Date() && + (payment?.withdrawableAmount ?? 0n) > 0n ); }); From 471a63759e2adbded031914fdf244e9a05a9a62b Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:24:38 -0400 Subject: [PATCH 19/58] Throw error if type of change isn't Add, Remove, Update --- src/hooks/utils/useCreateRoles.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 84705edb5..ac43af9bf 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -478,7 +478,7 @@ export default function useCreateRoles() { }), targetAddress: topHatAccount, }); - } else { + } else if (formHat.editedRole.status === EditBadgeStatus.Updated) { if ( formHat.editedRole.status === EditBadgeStatus.Updated && (formHat.editedRole.fieldNames.includes('roleName') || @@ -614,11 +614,7 @@ export default function useCreateRoles() { }); } } - - if ( - formHat.editedRole.status === EditBadgeStatus.Updated && - formHat.editedRole.fieldNames.includes('payments') - ) { + if (formHat.editedRole.fieldNames.includes('payments')) { /** * Updated Role Payments * Transfer hat to DAO, flush edited active streams, cancel edited streams, transfer hat to back to wearer, create new streams @@ -734,6 +730,8 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } } + } else { + throw new Error('Invalid Edited Status'); } } From e6cda3de7c56c8b00846eda5e12bd0b3d974acb7 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:24:48 -0400 Subject: [PATCH 20/58] Tighten up some typing --- src/hooks/utils/useCreateRoles.ts | 48 +++++++++++++------------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index ac43af9bf..d41ee3298 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -384,9 +384,8 @@ export default function useCreateRoles() { allTxs.push(mintHatTx(newHatId, formHat)); allTxs.push(createSmartAccountTx(BigInt(newHatId))); - const newStreams = - !!formHat?.payments && formHat.payments.filter(payment => !payment.streamId); - if (!!newStreams && newStreams.length > 0) { + const newStreams = (formHat.payments ?? []).filter(payment => !payment.streamId); + if (newStreams.length > 0) { const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); const newStreamTxData = createBatchLinearStreamCreationTx( newStreams, @@ -404,10 +403,10 @@ export default function useCreateRoles() { throw new Error('Cannot prepare transactions for removed role without wearer'); } - const fundsToClaimStreams = formHat?.payments?.filter( + const fundsToClaimStreams = (formHat.payments ?? []).filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n, ); - if (fundsToClaimStreams && fundsToClaimStreams.length) { + if (fundsToClaimStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -434,10 +433,10 @@ export default function useCreateRoles() { } } - const streamsToCancel = formHat?.payments?.filter( + const streamsToCancel = (formHat.payments ?? []).filter( payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), ); - if (!!streamsToCancel && streamsToCancel.length) { + if (streamsToCancel.length) { for (const stream of streamsToCancel) { if (!stream.streamId || !stream.contractAddress) { throw new Error( @@ -447,10 +446,7 @@ export default function useCreateRoles() { allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); } } - if ( - (streamsToCancel && streamsToCancel.length) || - (fundsToClaimStreams && fundsToClaimStreams.length) - ) { + if (streamsToCancel.length || fundsToClaimStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -480,9 +476,8 @@ export default function useCreateRoles() { }); } else if (formHat.editedRole.status === EditBadgeStatus.Updated) { if ( - formHat.editedRole.status === EditBadgeStatus.Updated && - (formHat.editedRole.fieldNames.includes('roleName') || - formHat.editedRole.fieldNames.includes('roleDescription')) + formHat.editedRole.fieldNames.includes('roleName') || + formHat.editedRole.fieldNames.includes('roleDescription') ) { /** * Updated Role Name or Description Transaction @@ -506,10 +501,7 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); } - if ( - formHat.editedRole.status === EditBadgeStatus.Updated && - formHat.editedRole.fieldNames.includes('member') - ) { + if (formHat.editedRole.fieldNames.includes('member')) { /** * Updated Role Member * Transfer hat to DAO, flush inactive and unedited streams, transfer hat to new wearer @@ -522,13 +514,13 @@ export default function useCreateRoles() { if (!originalHat) { throw new Error('Cannot find original hat'); } - const inactiveFundsToClaimStream = formHat?.payments?.filter( + const inactiveFundsToClaimStream = (formHat.payments ?? []).filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n && ((!!payment.endDate && payment.endDate > new Date()) || !!payment.isCancelled), ); - if (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) { + if (inactiveFundsToClaimStream.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -555,7 +547,7 @@ export default function useCreateRoles() { } } - const unEditedActiveStreams = formHat?.payments?.filter(payment => { + const unEditedActiveStreams = (formHat.payments ?? []).filter(payment => { if (payment.streamId === undefined || payment.endDate === undefined) { return false; } @@ -571,7 +563,7 @@ export default function useCreateRoles() { ); }); - if (unEditedActiveStreams && unEditedActiveStreams.length) { + if (unEditedActiveStreams.length) { for (const stream of unEditedActiveStreams) { if (!stream.streamId || !stream.contractAddress) { throw new Error( @@ -623,7 +615,7 @@ export default function useCreateRoles() { if (!formHat.wearer || !formHat.smartAddress) { throw new Error('Cannot prepare transactions'); } - const editedStreams = formHat?.payments?.filter(payment => { + const editedStreams = (formHat.payments ?? []).filter(payment => { if (payment.streamId === undefined) { return false; } @@ -638,10 +630,10 @@ export default function useCreateRoles() { payment.endDate > new Date() ); }); - const editedStreamsWithFundsToClaim = editedStreams?.filter( + const editedStreamsWithFundsToClaim = editedStreams.filter( stream => (stream?.withdrawableAmount ?? 0n) > 0n, ); - if (editedStreamsWithFundsToClaim && editedStreamsWithFundsToClaim.length) { + if (editedStreamsWithFundsToClaim.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -675,7 +667,7 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); } - if (editedStreams && editedStreams.length) { + if (editedStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -716,8 +708,8 @@ export default function useCreateRoles() { * New Streams * Create new streams */ - const newStreams = formHat?.payments?.filter(payment => !payment.streamId); - if (newStreams && newStreams.length) { + const newStreams = (formHat.payments ?? []).filter(payment => !payment.streamId); + if (newStreams.length) { if (!formHat.smartAddress || !formHat.wearer) { throw new Error('Cannot prepare transactions, missing data for new streams'); } From a5d3d81d5a55264467f807dd33e1fe6af4daacf0 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:27:12 -0400 Subject: [PATCH 21/58] Add newline --- src/hooks/utils/useCreateRoles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index d41ee3298..fcda00066 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -114,6 +114,7 @@ export default function useCreateRoles() { const ipfsClient = useIPFSClient(); const publicClient = usePublicClient(); const navigate = useNavigate(); + const submitProposalSuccessCallback = useCallback(() => { if (daoAddress) { navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); From 7fd1402a420cbc0019ce26899bd19a50f8ad6e4d Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:28:27 -0400 Subject: [PATCH 22/58] Rename function --- src/hooks/utils/useCreateRoles.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index fcda00066..2bf4c094d 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -349,7 +349,7 @@ export default function useCreateRoles() { [prepareBatchLinearStreamCreation], ); - const prepareAllTxs = useCallback( + const prepareCreateRolesModificationsProposalData = useCallback( async (modifiedHats: RoleHatFormValueEdited[]) => { if (!hatsTree || !daoAddress) { throw new Error('Cannot prepare transactions'); @@ -796,7 +796,7 @@ export default function useCreateRoles() { throw new Error('Cannot edit Roles without a HatsTree'); } - const allTxs = await prepareAllTxs(modifiedHats); + const allTxs = await prepareCreateRolesModificationsProposalData(modifiedHats); proposalData = { targets: allTxs.map(({ targetAddress }) => targetAddress), @@ -828,7 +828,7 @@ export default function useCreateRoles() { hatsTree, submitProposal, prepareCreateTopHatProposalData, - prepareAllTxs, + prepareCreateRolesModificationsProposalData, t, submitProposalSuccessCallback, uploadHatDescription, From 6ee2d6a1b543116a77ed05e1240495a167693366 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:37:37 -0400 Subject: [PATCH 23/58] Consolidate all functions INTO the useCreateRoles hook --- src/hooks/utils/useCreateRoles.ts | 167 ++++++++++++++---------------- 1 file changed, 78 insertions(+), 89 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 2bf4c094d..4d37a2e0b 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -29,66 +29,6 @@ import useSubmitProposal from '../DAO/proposal/useSubmitProposal'; import useCreateSablierStream from '../streams/useCreateSablierStream'; import { predictAccountAddress } from './../../store/roles/rolesStoreUtils'; -const hatsDetailsBuilder = (data: { name: string; description: string }) => { - return JSON.stringify({ - type: '1.0', - data, - }); -}; - -const createHatStruct = async ( - name: string, - description: string, - wearer: Address, - uploadHatDescription: (hatDescription: string) => Promise, -) => { - const details = await uploadHatDescription( - hatsDetailsBuilder({ - name: name, - description: description, - }), - ); - - const newHat: HatStruct = { - maxSupply: 1, - details, - imageURI: '', - isMutable: true, - wearer: wearer, - }; - - return newHat; -}; - -const createHatStructFromRoleFormValues = async ( - role: RoleHatFormValueEdited, - uploadHatDescription: (hatDescription: string) => Promise, -) => { - if (role.name === undefined || role.description === undefined) { - throw new Error('Hat name or description of added hat is undefined.'); - } - - if (role.wearer === undefined) { - throw new Error('Hat wearer of added hat is undefined.'); - } - - return createHatStruct( - role.name, - role.description, - getAddress(role.wearer), - uploadHatDescription, - ); -}; - -const createHatStructsFromRolesFormValues = async ( - modifiedRoles: RoleHatFormValueEdited[], - uploadHatDescription: (hatDescription: string) => Promise, -) => { - return Promise.all( - modifiedRoles.map(role => createHatStructFromRoleFormValues(role, uploadHatDescription)), - ); -}; - export default function useCreateRoles() { const { node: { safe, daoAddress, daoName }, @@ -115,11 +55,12 @@ export default function useCreateRoles() { const publicClient = usePublicClient(); const navigate = useNavigate(); - const submitProposalSuccessCallback = useCallback(() => { - if (daoAddress) { - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); - } - }, [daoAddress, addressPrefix, navigate]); + const hatsDetailsBuilder = useCallback((data: { name: string; description: string }) => { + return JSON.stringify({ + type: '1.0', + data, + }); + }, []); const uploadHatDescription = useCallback( async (hatDescription: string) => { @@ -129,6 +70,50 @@ export default function useCreateRoles() { [ipfsClient], ); + const createHatStruct = useCallback( + async (name: string, description: string, wearer: Address) => { + const details = await uploadHatDescription( + hatsDetailsBuilder({ + name: name, + description: description, + }), + ); + + const newHat: HatStruct = { + maxSupply: 1, + details, + imageURI: '', + isMutable: true, + wearer: wearer, + }; + + return newHat; + }, + [hatsDetailsBuilder, uploadHatDescription], + ); + + const createHatStructFromRoleFormValues = useCallback( + async (role: RoleHatFormValueEdited) => { + if (role.name === undefined || role.description === undefined) { + throw new Error('Hat name or description of added hat is undefined.'); + } + + if (role.wearer === undefined) { + throw new Error('Hat wearer of added hat is undefined.'); + } + + return createHatStruct(role.name, role.description, getAddress(role.wearer)); + }, + [createHatStruct], + ); + + const createHatStructsFromRolesFormValues = useCallback( + async (modifiedRoles: RoleHatFormValueEdited[]) => { + return Promise.all(modifiedRoles.map(role => createHatStructFromRoleFormValues(role))); + }, + [createHatStructFromRoleFormValues], + ); + const predictSmartAccount = useCallback( async (hatId: bigint) => { if (!publicClient) { @@ -220,13 +205,14 @@ export default function useCreateRoles() { }, [ daoAddress, - decentHatsMasterCopy, - uploadHatDescription, daoName, - hatsProtocol, - hatsAccount1ofNMasterCopy, + decentHatsMasterCopy, erc6551Registry, + hatsAccount1ofNMasterCopy, + hatsDetailsBuilder, + hatsProtocol, keyValuePairs, + uploadHatDescription, ], ); @@ -258,7 +244,6 @@ export default function useCreateRoles() { formRole.name, formRole.description, getAddress(formRole.wearer), - uploadHatDescription, ); return { @@ -278,7 +263,7 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }; }, - [uploadHatDescription, hatsProtocol], + [createHatStruct, hatsProtocol], ); const mintHatTx = useCallback( @@ -731,19 +716,20 @@ export default function useCreateRoles() { return allTxs; }, [ - daoAddress, - hatsProtocol, - predictSmartAccount, - prepareCancelStreamTx, - uploadHatDescription, + createBatchLinearStreamCreationTx, createHatTx, createSmartAccountTx, + daoAddress, getHat, getPayment, + hatsDetailsBuilder, + hatsProtocol, hatsTree, mintHatTx, + predictSmartAccount, + prepareCancelStreamTx, prepareHatsAccountFlushExecData, - createBatchLinearStreamCreationTx, + uploadHatDescription, ], ); @@ -783,10 +769,7 @@ export default function useCreateRoles() { ); } - const newHatStructs = await createHatStructsFromRolesFormValues( - modifiedHats, - uploadHatDescription, - ); + const newHatStructs = await createHatStructsFromRolesFormValues(modifiedHats); proposalData = await prepareCreateTopHatProposalData( values.proposalMetadata, newHatStructs, @@ -813,7 +796,11 @@ export default function useCreateRoles() { pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - successCallback: submitProposalSuccessCallback, + successCallback: () => { + if (daoAddress) { + navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); + } + }, }); } catch (e) { console.error(e); @@ -823,16 +810,18 @@ export default function useCreateRoles() { } }, [ - safe, - hatsTreeId, + createHatStructsFromRolesFormValues, hatsTree, - submitProposal, - prepareCreateTopHatProposalData, + hatsTreeId, prepareCreateRolesModificationsProposalData, - t, - submitProposalSuccessCallback, - uploadHatDescription, + prepareCreateTopHatProposalData, publicClient, + safe, + submitProposal, + t, + navigate, + addressPrefix, + daoAddress, ], ); From 6033227a495bbb535c7f156fbad358a832011300 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 16:46:52 -0400 Subject: [PATCH 24/58] Standardize the function signatures and logic for new tree vs existing tree prop creation --- src/hooks/utils/useCreateRoles.ts | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 4d37a2e0b..0319462be 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -140,7 +140,7 @@ export default function useCreateRoles() { ); const prepareCreateTopHatProposalData = useCallback( - async (proposalMetadata: CreateProposalMetadata, addedHats: HatStruct[]) => { + async (proposalMetadata: CreateProposalMetadata, modifiedHats: RoleHatFormValueEdited[]) => { if (!daoAddress) { throw new Error('Can not create top hat without DAO Address'); } @@ -179,6 +179,8 @@ export default function useCreateRoles() { wearer: zeroAddress, }; + const addedHats = await createHatStructsFromRolesFormValues(modifiedHats); + const createAndDeclareTreeData = encodeFunctionData({ abi: DecentHatsAbi, functionName: 'createAndDeclareTree', @@ -213,6 +215,7 @@ export default function useCreateRoles() { hatsProtocol, keyValuePairs, uploadHatDescription, + createHatStructsFromRolesFormValues, ], ); @@ -335,7 +338,7 @@ export default function useCreateRoles() { ); const prepareCreateRolesModificationsProposalData = useCallback( - async (modifiedHats: RoleHatFormValueEdited[]) => { + async (proposalMetadata: CreateProposalMetadata, modifiedHats: RoleHatFormValueEdited[]) => { if (!hatsTree || !daoAddress) { throw new Error('Cannot prepare transactions'); } @@ -713,7 +716,12 @@ export default function useCreateRoles() { } } - return allTxs; + return { + targets: allTxs.map(({ targetAddress }) => targetAddress), + calldatas: allTxs.map(({ calldata }) => calldata), + values: allTxs.map(() => 0n), + metaData: proposalMetadata, + }; }, [ createBatchLinearStreamCreationTx, @@ -769,24 +777,19 @@ export default function useCreateRoles() { ); } - const newHatStructs = await createHatStructsFromRolesFormValues(modifiedHats); proposalData = await prepareCreateTopHatProposalData( values.proposalMetadata, - newHatStructs, + modifiedHats, ); } else { if (!hatsTree) { throw new Error('Cannot edit Roles without a HatsTree'); } - const allTxs = await prepareCreateRolesModificationsProposalData(modifiedHats); - - proposalData = { - targets: allTxs.map(({ targetAddress }) => targetAddress), - calldatas: allTxs.map(({ calldata }) => calldata), - values: allTxs.map(() => 0n), - metaData: values.proposalMetadata, - }; + proposalData = await prepareCreateRolesModificationsProposalData( + values.proposalMetadata, + modifiedHats, + ); } // // All done, submit the proposal! @@ -810,18 +813,17 @@ export default function useCreateRoles() { } }, [ - createHatStructsFromRolesFormValues, + addressPrefix, + daoAddress, hatsTree, hatsTreeId, + navigate, prepareCreateRolesModificationsProposalData, prepareCreateTopHatProposalData, publicClient, safe, submitProposal, t, - navigate, - addressPrefix, - daoAddress, ], ); From 72ea878c92e736e2e7bfde7392c72563d69ac80b Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 5 Sep 2024 17:14:22 -0400 Subject: [PATCH 25/58] Update a comment --- src/hooks/utils/useCreateRoles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 0319462be..44123bc63 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -598,7 +598,7 @@ export default function useCreateRoles() { if (formHat.editedRole.fieldNames.includes('payments')) { /** * Updated Role Payments - * Transfer hat to DAO, flush edited active streams, cancel edited streams, transfer hat to back to wearer, create new streams + * Transfer hat to DAO if there are funds to claim, flush edited active streams with funds to claim, cancel edited streams, transfer hat back to wearer if necessary */ if (!formHat.wearer || !formHat.smartAddress) { From 52e428856e097520bf09cfa7535383a67c491177 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:10:00 -0400 Subject: [PATCH 26/58] transfer deleted role wearer to DAO (and keep it there) --- src/hooks/utils/useCreateRoles.ts | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 44123bc63..e4fed0f98 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -388,22 +388,19 @@ export default function useCreateRoles() { * Removed Role * Transfer hat to DAO, flush streams, cancel streams, transfer hat to back to wearer, set hat status to false */ - if (formHat.wearer === undefined || formHat.smartAddress === undefined) { - throw new Error('Cannot prepare transactions for removed role without wearer'); - } + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); const fundsToClaimStreams = (formHat.payments ?? []).filter( payment => (payment?.withdrawableAmount ?? 0n) > 0n, ); if (fundsToClaimStreams.length) { - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], - }), - targetAddress: hatsProtocol, - }); for (const stream of fundsToClaimStreams) { if (!stream.streamId || !stream.contractAddress) { throw new Error( @@ -435,16 +432,6 @@ export default function useCreateRoles() { allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); } } - if (streamsToCancel.length || fundsToClaimStreams.length) { - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], - }), - targetAddress: hatsProtocol, - }); - } allTxs.push({ calldata: encodeFunctionData({ From 40a36cebbb5d7049ee69cfc610821aa82c6c3bfe Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:11:02 -0400 Subject: [PATCH 27/58] move typing error throwing (less occurances) and update messages --- src/hooks/utils/useCreateRoles.ts | 33 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index e4fed0f98..65424b2ea 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -340,7 +340,7 @@ export default function useCreateRoles() { const prepareCreateRolesModificationsProposalData = useCallback( async (proposalMetadata: CreateProposalMetadata, modifiedHats: RoleHatFormValueEdited[]) => { if (!hatsTree || !daoAddress) { - throw new Error('Cannot prepare transactions'); + throw new Error('Cannot prepare transactions without hats tree or DAO address'); } const topHatAccount = hatsTree.topHat.smartAddress; @@ -357,6 +357,15 @@ export default function useCreateRoles() { for (let index = 0; index < modifiedHats.length; index++) { const formHat = modifiedHats[index]; + if ( + formHat.name === undefined || + formHat.description === undefined || + formHat.wearer === undefined + ) { + throw new Error('Role details are missing', { + cause: formHat, + }); + } if (formHat.editedRole.status === EditBadgeStatus.New) { /** @@ -388,6 +397,11 @@ export default function useCreateRoles() { * Removed Role * Transfer hat to DAO, flush streams, cancel streams, transfer hat to back to wearer, set hat status to false */ + if (formHat.smartAddress === undefined) { + throw new Error( + 'Cannot prepare transactions for removed role without smart account address', + ); + } allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -459,9 +473,6 @@ export default function useCreateRoles() { * Updated Role Name or Description Transaction * Upload the new details to IPFS, Change hat details */ - if (formHat.name === undefined || formHat.description === undefined) { - throw new Error('Hat name or description of existing hat is undefined.'); - } const details = await uploadHatDescription( hatsDetailsBuilder({ name: formHat.name, @@ -483,8 +494,8 @@ export default function useCreateRoles() { * Transfer hat to DAO, flush inactive and unedited streams, transfer hat to new wearer */ - if (formHat.wearer === undefined || formHat.smartAddress === undefined) { - throw new Error('Cannot prepare transactions for edited role without wearer'); + if (formHat.smartAddress === undefined) { + throw new Error('Cannot prepare transactions for edited role without smart address'); } const originalHat = getHat(formHat.id); if (!originalHat) { @@ -588,8 +599,8 @@ export default function useCreateRoles() { * Transfer hat to DAO if there are funds to claim, flush edited active streams with funds to claim, cancel edited streams, transfer hat back to wearer if necessary */ - if (!formHat.wearer || !formHat.smartAddress) { - throw new Error('Cannot prepare transactions'); + if (!formHat.smartAddress) { + throw new Error('Cannot prepare transactions for edited role without smart address'); } const editedStreams = (formHat.payments ?? []).filter(payment => { if (payment.streamId === undefined) { @@ -686,8 +697,10 @@ export default function useCreateRoles() { */ const newStreams = (formHat.payments ?? []).filter(payment => !payment.streamId); if (newStreams.length) { - if (!formHat.smartAddress || !formHat.wearer) { - throw new Error('Cannot prepare transactions, missing data for new streams'); + if (!formHat.smartAddress) { + throw new Error( + 'Cannot prepare transactions for edited role without smart address', + ); } const newPredictedHatSmartAccount = await predictSmartAccount(BigInt(formHat.id)); const newStreamTxData = createBatchLinearStreamCreationTx( From d00f355ef4b55b54f28e8036b397fad62b4f58e9 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 6 Sep 2024 02:38:52 -0400 Subject: [PATCH 28/58] update box shadow and bg --- src/components/pages/Roles/RolePaymentDetails.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index b87a9082e..771bed546 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Address, getAddress } from 'viem'; import { useAccount, usePublicClient } from 'wagmi'; -import { DETAILS_SHADOW } from '../../../constants/common'; import { DAO_ROUTES } from '../../../constants/routes'; import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; @@ -170,11 +169,11 @@ export function RolePaymentDetails({ return ( @@ -283,7 +282,7 @@ export function RolePaymentDetails({ borderLeft="1px solid" borderColor="white-alpha-08" h="full" - boxShadow={DETAILS_SHADOW} + boxShadow="0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset" w="0" /> From 7e16159619d771a585a062f9a43fdfafa47a5563 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Fri, 6 Sep 2024 09:17:20 -0400 Subject: [PATCH 29/58] Update comment --- src/hooks/utils/useCreateRoles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 65424b2ea..3bf326ed2 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -395,7 +395,7 @@ export default function useCreateRoles() { } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { /** * Removed Role - * Transfer hat to DAO, flush streams, cancel streams, transfer hat to back to wearer, set hat status to false + * Transfer hat to DAO, flush streams, cancel streams, set hat status to false */ if (formHat.smartAddress === undefined) { throw new Error( From 074db2f0ebfa575090b915429567b2bf83f36989 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Fri, 6 Sep 2024 09:32:09 -0400 Subject: [PATCH 30/58] No need for truthy check --- src/hooks/utils/useCreateRoles.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 3bf326ed2..c9e29b4a1 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -569,10 +569,7 @@ export default function useCreateRoles() { } } // transfer hat from DAO to new wearer if there are any funds to claim - if ( - (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || - (unEditedActiveStreams && unEditedActiveStreams.length) - ) { + if (inactiveFundsToClaimStream.length || unEditedActiveStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, From ba5447b03bd3c707a72eaab4c4504e499e6f9e58 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Fri, 6 Sep 2024 11:06:00 -0400 Subject: [PATCH 31/58] Move all "filtering" logic into own functions --- src/hooks/utils/useCreateRoles.ts | 228 +++++++++++++++++++----------- 1 file changed, 142 insertions(+), 86 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index c9e29b4a1..04473f7dd 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -337,6 +337,79 @@ export default function useCreateRoles() { [prepareBatchLinearStreamCreation], ); + const getUneditedActiveStreamsFromFormHat = useCallback( + (formHat: RoleHatFormValueEdited) => + (formHat.payments ?? []).filter(payment => { + if (payment.streamId === undefined || payment.endDate === undefined) { + return false; + } + const originalPayment = getPayment(formHat.id, payment.streamId); + if (originalPayment === null) { + return false; + } + return ( + isEqual(payment, originalPayment) && + !payment.isCancelled && + payment.endDate < new Date() && + (payment?.withdrawableAmount ?? 0n) > 0n + ); + }), + [getPayment], + ); + + const getInactiveStreamsWithFundsToClaimFromFromHat = useCallback( + (formHat: RoleHatFormValueEdited) => { + return (formHat.payments ?? []).filter( + payment => + (payment?.withdrawableAmount ?? 0n) > 0n && + ((!!payment.endDate && payment.endDate > new Date()) || !!payment.isCancelled), + ); + }, + [], + ); + + const getNewStreamsFromFormHat = useCallback((formHat: RoleHatFormValueEdited) => { + return (formHat.payments ?? []).filter(payment => !payment.streamId); + }, []); + + const getStreamsWithFundsToClaimFromFormHat = useCallback((formHat: RoleHatFormValueEdited) => { + return (formHat.payments ?? []).filter(payment => (payment?.withdrawableAmount ?? 0n) > 0n); + }, []); + + const getActiveStreamsFromFormHat = useCallback((formHat: RoleHatFormValueEdited) => { + return (formHat.payments ?? []).filter( + payment => !payment.isCancelled && !!payment.endDate && payment.endDate > new Date(), + ); + }, []); + + const getEditedStreamsFromFormHat = useCallback( + (formHat: RoleHatFormValueEdited) => { + return (formHat.payments ?? []).filter(payment => { + if (payment.streamId === undefined) { + return false; + } + const originalPayment = getPayment(formHat.id, payment.streamId); + if (originalPayment === null) { + return false; + } + return ( + !isEqual(payment, originalPayment) && + !payment.isCancelled && + payment.endDate && + payment.endDate > new Date() + ); + }); + }, + [getPayment], + ); + + const getStreamsWithFundsToClaimFromFormStreams = useCallback( + (streams: SablierPaymentFormValues[]) => { + return streams.filter(stream => (stream?.withdrawableAmount ?? 0n) > 0n); + }, + [], + ); + const prepareCreateRolesModificationsProposalData = useCallback( async (proposalMetadata: CreateProposalMetadata, modifiedHats: RoleHatFormValueEdited[]) => { if (!hatsTree || !daoAddress) { @@ -352,8 +425,39 @@ export default function useCreateRoles() { // so that we can correctly predict the hatId for the "create new role" transaction let newHatCount = 0; - // "active stream" = not cancelled and not past end date - // "inactive stream" = cancelled or past end date + // The Algorithm + // + // for each modified role + // + // New Role + // - allTxs.push(create hat) + // - allTxs.push(mint hat) + // - allTxs.push(create smart account) + // - does it have any streams? + // - allTxs.push(create new streams transactions datas) + // Deleted Role + // - for each inactive stream with funds to claim + // - allTxs.push(flush stream transaction data) + // - for each active stream + // - allTxs.push(flush stream transaction data) + // - allTxs.push(cancel stream transaction data) + // - allTxs.push(deactivate role transaction data) + // Edited Role + // - is the name or description changed? + // - allTxs.push(edit details data) + // - is the member changed? + // - for each inactive streams with funds to claim + // - allTxs.push(flush stream transaction data) + // - for each active stream + // - if stream was not edited + // - allTxs.push(flush stream transaction data) + // - for each edited active streams + // - if stream was edited + // - allTxs.push(flush stream transaction data) + // - allTxs.push(cancel stream transaction data) + // - allTxs.push(create new stream transaction data) + // - for each new stream + // - allTxs.push(create new stream transactions datas) for (let index = 0; index < modifiedHats.length; index++) { const formHat = modifiedHats[index]; @@ -368,10 +472,6 @@ export default function useCreateRoles() { } if (formHat.editedRole.status === EditBadgeStatus.New) { - /** - * New Role - * Create new hat, mint it, create smart account, and create new streams - */ const newHatId = predictHatId({ adminHatId: hatsTree.adminHat.id, hatsCount: hatsTree.roleHatsTotalCount + newHatCount, @@ -382,7 +482,8 @@ export default function useCreateRoles() { allTxs.push(mintHatTx(newHatId, formHat)); allTxs.push(createSmartAccountTx(BigInt(newHatId))); - const newStreams = (formHat.payments ?? []).filter(payment => !payment.streamId); + const newStreams = getNewStreamsFromFormHat(formHat); + if (newStreams.length > 0) { const newPredictedHatSmartAccount = await predictSmartAccount(newHatId); const newStreamTxData = createBatchLinearStreamCreationTx( @@ -393,10 +494,6 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } } else if (formHat.editedRole.status === EditBadgeStatus.Removed) { - /** - * Removed Role - * Transfer hat to DAO, flush streams, cancel streams, set hat status to false - */ if (formHat.smartAddress === undefined) { throw new Error( 'Cannot prepare transactions for removed role without smart account address', @@ -411,11 +508,10 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); - const fundsToClaimStreams = (formHat.payments ?? []).filter( - payment => (payment?.withdrawableAmount ?? 0n) > 0n, - ); - if (fundsToClaimStreams.length) { - for (const stream of fundsToClaimStreams) { + const streamsWithFundsToClaim = getStreamsWithFundsToClaimFromFormHat(formHat); + + if (streamsWithFundsToClaim.length) { + for (const stream of streamsWithFundsToClaim) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for flush stream transaction', @@ -433,11 +529,10 @@ export default function useCreateRoles() { } } - const streamsToCancel = (formHat.payments ?? []).filter( - payment => !!payment.endDate && !payment.isCancelled && payment.endDate > new Date(), - ); - if (streamsToCancel.length) { - for (const stream of streamsToCancel) { + const activeStreams = getActiveStreamsFromFormHat(formHat); + + if (activeStreams.length) { + for (const stream of activeStreams) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for cancel stream transaction', @@ -469,10 +564,6 @@ export default function useCreateRoles() { formHat.editedRole.fieldNames.includes('roleName') || formHat.editedRole.fieldNames.includes('roleDescription') ) { - /** - * Updated Role Name or Description Transaction - * Upload the new details to IPFS, Change hat details - */ const details = await uploadHatDescription( hatsDetailsBuilder({ name: formHat.name, @@ -489,11 +580,6 @@ export default function useCreateRoles() { }); } if (formHat.editedRole.fieldNames.includes('member')) { - /** - * Updated Role Member - * Transfer hat to DAO, flush inactive and unedited streams, transfer hat to new wearer - */ - if (formHat.smartAddress === undefined) { throw new Error('Cannot prepare transactions for edited role without smart address'); } @@ -501,13 +587,11 @@ export default function useCreateRoles() { if (!originalHat) { throw new Error('Cannot find original hat'); } - const inactiveFundsToClaimStream = (formHat.payments ?? []).filter( - payment => - (payment?.withdrawableAmount ?? 0n) > 0n && - ((!!payment.endDate && payment.endDate > new Date()) || !!payment.isCancelled), - ); - if (inactiveFundsToClaimStream.length) { + const inactiveStreamsWithFundsToClaim = + getInactiveStreamsWithFundsToClaimFromFromHat(formHat); + + if (inactiveStreamsWithFundsToClaim.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -516,7 +600,7 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - for (const stream of inactiveFundsToClaimStream) { + for (const stream of inactiveStreamsWithFundsToClaim) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for flush stream transaction', @@ -534,24 +618,10 @@ export default function useCreateRoles() { } } - const unEditedActiveStreams = (formHat.payments ?? []).filter(payment => { - if (payment.streamId === undefined || payment.endDate === undefined) { - return false; - } - const originalPayment = getPayment(formHat.id, payment.streamId); - if (originalPayment === null) { - return false; - } - return ( - isEqual(payment, originalPayment) && - !payment.isCancelled && - payment.endDate < new Date() && - (payment?.withdrawableAmount ?? 0n) > 0n - ); - }); + const uneditedActiveStreams = getUneditedActiveStreamsFromFormHat(formHat); - if (unEditedActiveStreams.length) { - for (const stream of unEditedActiveStreams) { + if (uneditedActiveStreams.length) { + for (const stream of uneditedActiveStreams) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for flush stream transaction', @@ -568,8 +638,9 @@ export default function useCreateRoles() { }); } } - // transfer hat from DAO to new wearer if there are any funds to claim - if (inactiveFundsToClaimStream.length || unEditedActiveStreams.length) { + + if (inactiveStreamsWithFundsToClaim.length || uneditedActiveStreams.length) { + // because the DAO currently owns the Hat allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -579,7 +650,7 @@ export default function useCreateRoles() { targetAddress: hatsProtocol, }); } else { - // transfer hat from original wearer to new wearer if there are no funds to claim + // because the original wearer currently owns the Hat allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -591,32 +662,14 @@ export default function useCreateRoles() { } } if (formHat.editedRole.fieldNames.includes('payments')) { - /** - * Updated Role Payments - * Transfer hat to DAO if there are funds to claim, flush edited active streams with funds to claim, cancel edited streams, transfer hat back to wearer if necessary - */ - if (!formHat.smartAddress) { throw new Error('Cannot prepare transactions for edited role without smart address'); } - const editedStreams = (formHat.payments ?? []).filter(payment => { - if (payment.streamId === undefined) { - return false; - } - const originalPayment = getPayment(formHat.id, payment.streamId); - if (originalPayment === null) { - return false; - } - return ( - !isEqual(payment, originalPayment) && - !payment.isCancelled && - payment.endDate && - payment.endDate > new Date() - ); - }); - const editedStreamsWithFundsToClaim = editedStreams.filter( - stream => (stream?.withdrawableAmount ?? 0n) > 0n, - ); + + const editedStreams = getEditedStreamsFromFormHat(formHat); + const editedStreamsWithFundsToClaim = + getStreamsWithFundsToClaimFromFormStreams(editedStreams); + if (editedStreamsWithFundsToClaim.length) { allTxs.push({ calldata: encodeFunctionData({ @@ -688,11 +741,8 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } - /** - * New Streams - * Create new streams - */ - const newStreams = (formHat.payments ?? []).filter(payment => !payment.streamId); + + const newStreams = getNewStreamsFromFormHat(formHat); if (newStreams.length) { if (!formHat.smartAddress) { throw new Error( @@ -725,8 +775,14 @@ export default function useCreateRoles() { createHatTx, createSmartAccountTx, daoAddress, + getActiveStreamsFromFormHat, + getEditedStreamsFromFormHat, getHat, - getPayment, + getInactiveStreamsWithFundsToClaimFromFromHat, + getNewStreamsFromFormHat, + getStreamsWithFundsToClaimFromFormHat, + getStreamsWithFundsToClaimFromFormStreams, + getUneditedActiveStreamsFromFormHat, hatsDetailsBuilder, hatsProtocol, hatsTree, From d4b506f8360ecd01aad59c03025fdf791bb26b28 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Fri, 6 Sep 2024 13:42:49 -0400 Subject: [PATCH 32/58] Fix comment --- src/hooks/utils/useCreateRoles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 04473f7dd..837056034 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -845,7 +845,7 @@ export default function useCreateRoles() { ); } - // // All done, submit the proposal! + // All done, submit the proposal! await submitProposal({ proposalData, nonce: values.customNonce ?? safe.nextNonce, From 00c504f9ecc27f313cec293ba272b167acba2526 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:43:55 -0400 Subject: [PATCH 33/58] gray out canceled and past end date streams --- .../pages/Roles/RolePaymentDetails.tsx | 24 +++++++++++++++++-- .../Roles/forms/RoleFormPaymentStreams.tsx | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 771bed546..3584fdabb 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -75,6 +75,7 @@ interface RolePaymentDetailsProps { startDate: Date; endDate: Date; cliffDate?: Date; + isCancelled: boolean; isStreaming: () => boolean; withdrawableAmount?: bigint; }; @@ -167,16 +168,34 @@ export function RolePaymentDetails({ return Number(payment.amount.value) * foundAsset.usdPrice; }, [payment, assetsFungible]); + const activeStreamProps = useMemo(() => { + if (!payment.isCancelled && Date.now() < payment.endDate.getTime()) { + return { + bg: '#221D25', + sx: undefined, + }; + } else { + return { + sx: { + p: { + color: 'neutral-6', + }, + }, + bg: 'none', + }; + } + }, [payment]); + return ( - {canWithdraw && !!payment?.withdrawableAmount && payment.withdrawableAmount > 0n && ( + {canWithdraw && ( - {payments?.map((payment, index) => { - // @note don't render if form isn't valid - if (!payment.amount || !payment.asset || !payment.startDate || !payment.endDate) - return null; - return ( - - false, - }} - onClick={() => { - setFieldValue('roleEditing.roleEditingPaymentIndex', index); - }} - /> - - ); - })} + {payments + ?.sort(paymentSorterByWithdrawAmount) + .sort(paymentSorterByStartDate) + .sort(paymentSorterByActiveStatus) + .map((payment, index) => { + // @note don't render if form isn't valid + if (!payment.amount || !payment.asset || !payment.startDate || !payment.endDate) + return null; + return ( + + false, + }} + onClick={() => { + setFieldValue('roleEditing.roleEditingPaymentIndex', index); + }} + /> + + ); + })} )} diff --git a/src/store/roles/rolesStoreUtils.ts b/src/store/roles/rolesStoreUtils.ts index fb44255a1..d0a247123 100644 --- a/src/store/roles/rolesStoreUtils.ts +++ b/src/store/roles/rolesStoreUtils.ts @@ -290,3 +290,43 @@ export const predictHatId = ({ adminHatId, hatsCount }: { adminHatId: Hex; hatsC // Total length of Hat ID is **32 bytes** + 2 bytes for 0x return BigInt(`${adminLevelBinary}${newSiblingId}`.padEnd(66, '0')); }; + +export const isActive = (payment: { isCancelled?: boolean; endDate?: Date }) => { + const now = new Date(); + // A payment is active if it's not cancelled and its end date is in the future (or it doesn't have an end date yet) + return !payment.isCancelled && (payment.endDate === undefined || payment.endDate > now); +}; + +export const paymentSorterByActiveStatus = ( + a: { isCancelled?: boolean; endDate?: Date }, + b: { isCancelled?: boolean; endDate?: Date } +) => { + const aIsActive = isActive(a); + const bIsActive = isActive(b); + + if (aIsActive && !bIsActive) { + return -1; // 'a' is active and should come first + } + if (!aIsActive && bIsActive) { + return 1; // 'b' is active and should come first + } + + // If both are active or both inactive, maintain the current order + return 0; +}; +export const paymentSorterByStartDate = (a: { startDate?: Date }, b: { startDate?: Date }) => { + if (!a?.startDate) return 1; // No start date, move this payment last + if (!b?.startDate) return -1; // No start date, move b last + + return a.startDate.getTime() - b.startDate.getTime(); // Sort by earliest start date +}; + +export const paymentSorterByWithdrawAmount = ( + a: { withdrawableAmount?: bigint }, + b: { withdrawableAmount?: bigint }, +) => { + if (!a?.withdrawableAmount) return 1; // No withdrawable amount, move this payment last + if (!b?.withdrawableAmount) return -1; + + return Number(a.withdrawableAmount - b.withdrawableAmount); // Sort by amount +}; From 60a7b05604292bd2f25ebb8ee5ddcdd3b1a0d892 Mon Sep 17 00:00:00 2001 From: Kyrylo Klymenko Date: Mon, 9 Sep 2024 14:47:28 +0200 Subject: [PATCH 37/58] Do not allow clicking on soft-deleted roles --- src/components/pages/Roles/RoleCard.tsx | 4 +++- src/components/pages/Roles/RolesTable.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/pages/Roles/RoleCard.tsx b/src/components/pages/Roles/RoleCard.tsx index c97a243ab..b56636819 100644 --- a/src/components/pages/Roles/RoleCard.tsx +++ b/src/components/pages/Roles/RoleCard.tsx @@ -210,10 +210,12 @@ export function RoleCardEdit({ editStatus, handleRoleClick, }: RoleEditProps) { + const isRemovedRole = editStatus === EditBadgeStatus.Removed; return ( Date: Mon, 9 Sep 2024 16:10:14 +0200 Subject: [PATCH 38/58] Nipick --- src/components/pages/Roles/RoleCard.tsx | 2 +- src/components/pages/Roles/RolesTable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/Roles/RoleCard.tsx b/src/components/pages/Roles/RoleCard.tsx index b56636819..0e60b98c8 100644 --- a/src/components/pages/Roles/RoleCard.tsx +++ b/src/components/pages/Roles/RoleCard.tsx @@ -215,7 +215,7 @@ export function RoleCardEdit({ Date: Mon, 9 Sep 2024 10:16:11 -0400 Subject: [PATCH 39/58] create new array when sorting for form --- src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx index e5229e874..3a7a3d71c 100644 --- a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx +++ b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx @@ -34,8 +34,8 @@ export function RoleFormPaymentStreams() { {t('addPayment')} - {payments - ?.sort(paymentSorterByWithdrawAmount) + {[...(payments ?? [])] + .sort(paymentSorterByWithdrawAmount) .sort(paymentSorterByStartDate) .sort(paymentSorterByActiveStatus) .map((payment, index) => { From 04f1c1c036952a39ea9f5be5a021d83d38a824f7 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:16:27 -0400 Subject: [PATCH 40/58] pass isStreaming func if exisitng stream --- src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx index 3a7a3d71c..89ed21fba 100644 --- a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx +++ b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx @@ -52,7 +52,7 @@ export function RoleFormPaymentStreams() { startDate: payment.startDate, cliffDate: payment.cliffDate, isCancelled: payment.isCancelled ?? false, - isStreaming: () => false, + isStreaming: payment.isStreaming ?? (() => false), }} onClick={() => { setFieldValue('roleEditing.roleEditingPaymentIndex', index); From e9e8638a8a45e6aa2954aef6bf38eb696c6a9075 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Mon, 9 Sep 2024 11:02:03 -0400 Subject: [PATCH 41/58] Throw toast when IPFS infura errors --- src/providers/App/hooks/useIPFSClient.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/providers/App/hooks/useIPFSClient.ts b/src/providers/App/hooks/useIPFSClient.ts index f62b0c737..0c0059a48 100644 --- a/src/providers/App/hooks/useIPFSClient.ts +++ b/src/providers/App/hooks/useIPFSClient.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { useMemo, useCallback } from 'react'; +import { toast } from 'react-toastify'; const INFURA_AUTH = 'Basic ' + @@ -12,15 +13,26 @@ const axiosClient = axios.create({ baseURL: BASE_URL, headers: { Authorization: export default function useIPFSClient() { const cat = useCallback(async (hash: string) => { - const response = await axiosClient.post(`${BASE_URL}/cat?arg=${hash}`); - return response.data; + return axiosClient + .post(`${BASE_URL}/cat?arg=${hash}`) + .then(response => response.data) + .catch(error => { + console.error(error); + toast('Error fetching data from IPFS, please try again later.'); + }); }, []); const add = useCallback(async (data: string) => { const formData = new FormData(); formData.append('file', data); - const response = await axiosClient.post(`${BASE_URL}/add`, formData); - return response.data; + + return axiosClient + .post(`${BASE_URL}/add`, formData) + .then(response => response.data) + .catch(error => { + console.error(error); + toast('Error saving data to IPFS, please try again later.'); + }); }, []); const client = useMemo( From ea4553f43bd61a9b1cee38f47d41aac81e7a0a36 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:02:37 -0400 Subject: [PATCH 42/58] run pretty --- src/components/pages/Roles/RolesDetailsDrawerMobile.tsx | 7 ++++++- src/store/roles/rolesStoreUtils.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx index 9b8d3da3e..c19d4272c 100644 --- a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx @@ -4,7 +4,12 @@ import { useTranslation } from 'react-i18next'; import { getAddress, Hex } from 'viem'; import { isFeatureEnabled } from '../../../constants/common'; import { useFractal } from '../../../providers/App/AppProvider'; -import { paymentSorterByActiveStatus, paymentSorterByStartDate, paymentSorterByWithdrawAmount, useRolesStore } from '../../../store/roles'; +import { + paymentSorterByActiveStatus, + paymentSorterByStartDate, + paymentSorterByWithdrawAmount, + useRolesStore, +} from '../../../store/roles'; import DraggableDrawer from '../../ui/containers/DraggableDrawer'; import Divider from '../../ui/utils/Divider'; import { AvatarAndRoleName } from './RoleCard'; diff --git a/src/store/roles/rolesStoreUtils.ts b/src/store/roles/rolesStoreUtils.ts index d0a247123..6b2b37dbd 100644 --- a/src/store/roles/rolesStoreUtils.ts +++ b/src/store/roles/rolesStoreUtils.ts @@ -299,7 +299,7 @@ export const isActive = (payment: { isCancelled?: boolean; endDate?: Date }) => export const paymentSorterByActiveStatus = ( a: { isCancelled?: boolean; endDate?: Date }, - b: { isCancelled?: boolean; endDate?: Date } + b: { isCancelled?: boolean; endDate?: Date }, ) => { const aIsActive = isActive(a); const bIsActive = isActive(b); From 49399ea6986795bf6322bd0454a0b1531120583e Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:50:09 -0400 Subject: [PATCH 43/58] spread the array (create a new array before sorting) --- src/components/pages/Roles/RolesDetailsDrawer.tsx | 2 +- src/components/pages/Roles/RolesDetailsDrawerMobile.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/Roles/RolesDetailsDrawer.tsx b/src/components/pages/Roles/RolesDetailsDrawer.tsx index a23193f5e..fb353eaba 100644 --- a/src/components/pages/Roles/RolesDetailsDrawer.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawer.tsx @@ -172,7 +172,7 @@ export default function RolesDetailsDrawer({ > {t('payments')} - {roleHat.payments + {[...roleHat.payments] .sort(paymentSorterByWithdrawAmount) .sort(paymentSorterByStartDate) .sort(paymentSorterByActiveStatus) diff --git a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx index c19d4272c..a768b7252 100644 --- a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx @@ -105,7 +105,7 @@ export default function RolesDetailsDrawerMobile({ > {t('payments')} - {roleHat.payments + {[...roleHat.payments] .sort(paymentSorterByWithdrawAmount) .sort(paymentSorterByStartDate) .sort(paymentSorterByActiveStatus) From ad325e062da9345b93ded0c029764b8187bc88f6 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:52:10 -0400 Subject: [PATCH 44/58] move shadow to local const --- src/components/pages/Roles/RolePaymentDetails.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 078aa14c8..2cdcd2485 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -15,6 +15,9 @@ import { DEFAULT_DATE_FORMAT, formatCoin, formatUSD } from '../../../utils'; import { ModalType } from '../../ui/modals/ModalProvider'; import { useDecentModal } from '../../ui/modals/useDecentModal'; +const PAYMENT_DETAILS_BOX_SHADOW = + '0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset'; + function PaymentDate({ label, date }: { label: string; date?: Date }) { const { t } = useTranslation(['roles']); return ( @@ -188,7 +191,7 @@ export function RolePaymentDetails({ return ( @@ -301,7 +304,7 @@ export function RolePaymentDetails({ borderLeft="1px solid" borderColor="white-alpha-08" h="full" - boxShadow="0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset" + boxShadow={PAYMENT_DETAILS_BOX_SHADOW} w="0" /> From c251982bd9524ff254f8415a84e1de30bc721849 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:58:28 -0400 Subject: [PATCH 45/58] change to border for 'cancelled/ended' states --- .../pages/Roles/RolePaymentDetails.tsx | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 2cdcd2485..2f0ca9039 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -171,34 +171,42 @@ export function RolePaymentDetails({ return Number(payment.amount.value) * foundAsset.usdPrice; }, [payment, assetsFungible]); - const activeStreamProps = useMemo(() => { - if (!payment.isCancelled && Date.now() < payment.endDate.getTime()) { - return { - bg: '#221D25', - sx: undefined, - }; - } else { - return { - sx: { - p: { - color: 'neutral-6', + const isActiveStream = !payment.isCancelled && Date.now() < payment.endDate.getTime(); + + const activeStreamProps = useCallback( + (isTop: boolean) => + isActiveStream + ? { + bg: '#221D25', + sx: undefined, + boxShadow: PAYMENT_DETAILS_BOX_SHADOW, + } + : { + sx: { + p: { + color: 'neutral-6', + }, + }, + bg: 'none', + boxShadow: 'none', + borderTop: '1px solid', + borderBottom: isTop ? 'none' : '1px solid', + borderLeft: '1px solid', + borderRight: '1px solid', + borderColor: 'neutral-4', }, - }, - bg: 'none', - }; - } - }, [payment]); + [isActiveStream], + ); return ( Date: Mon, 9 Sep 2024 15:45:57 -0400 Subject: [PATCH 46/58] add cancel payment UI --- .../pages/Roles/RolePaymentDetails.tsx | 116 ++++++++++++++++-- .../Roles/forms/RoleFormPaymentStreams.tsx | 12 ++ src/i18n/locales/en/roles.json | 3 +- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 2f0ca9039..8323a0a78 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -1,11 +1,12 @@ -import { Box, Button, Flex, Grid, GridItem, Icon, Image, Text } from '@chakra-ui/react'; -import { Calendar, Download } from '@phosphor-icons/react'; +import { Box, Button, Flex, Grid, GridItem, Icon, IconButton, Image, Text } from '@chakra-ui/react'; +import { Calendar, DotsThree, Download, Trash } from '@phosphor-icons/react'; import { format } from 'date-fns'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { Address, getAddress } from 'viem'; +import { Address, getAddress, Hex } from 'viem'; import { useAccount, usePublicClient } from 'wagmi'; +import { NEUTRAL_2_82_TRANSPARENT, CARD_SHADOW } from '../../../constants/common'; import { DAO_ROUTES } from '../../../constants/routes'; import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; @@ -18,6 +19,88 @@ import { useDecentModal } from '../../ui/modals/useDecentModal'; const PAYMENT_DETAILS_BOX_SHADOW = '0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset'; +function CancelStreamMenu({ + onSuccess, + // hatId, + // streamId, +}: { + hatId: Hex; + streamId: string; + onSuccess: () => void; +}) { + const { t } = useTranslation(['roles']); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + + const handleCancelPayment = () => { + setTimeout(() => onSuccess(), 50); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + + setShowMenu(show => !show)} + /> + {showMenu && ( + + + + )} + + ); +} + function PaymentDate({ label, date }: { label: string; date?: Date }) { const { t } = useTranslation(['roles']); return ( @@ -63,6 +146,7 @@ function GreenStreamingDot({ isStreaming }: { isStreaming: boolean }) { } interface RolePaymentDetailsProps { + roleHatId?: Hex; roleHatWearerAddress?: Address; roleHatSmartAddress?: Address; payment: { @@ -84,6 +168,7 @@ interface RolePaymentDetailsProps { }; onClick?: () => void; showWithdraw?: boolean; + showCancel?: boolean; } export function RolePaymentDetails({ payment, @@ -91,6 +176,8 @@ export function RolePaymentDetails({ showWithdraw, roleHatWearerAddress, roleHatSmartAddress, + roleHatId, + showCancel, }: RolePaymentDetailsProps) { const { t } = useTranslation(['roles']); const { @@ -200,15 +287,23 @@ export function RolePaymentDetails({ return ( - + {showCancel && !!roleHatId && !!payment.streamId && ( + {}} + /> + )} + Date.now() + } payment={{ + streamId: payment.streamId, amount: payment.amount, asset: payment.asset, endDate: payment.endDate, diff --git a/src/i18n/locales/en/roles.json b/src/i18n/locales/en/roles.json index fdee444b5..af531f7c1 100644 --- a/src/i18n/locales/en/roles.json +++ b/src/i18n/locales/en/roles.json @@ -83,5 +83,6 @@ "roleInfoErrorPaymentFixedDateEndDateRequired": "End date is required.", "roleInfoErrorPaymentFixedDateEndDateAfterStartDate": "End date must be after start date.", "roleInfoErrorPaymentScheduleOrFixedDateRequired": "Payment schedule or fixed dates are required.", - "roleInfoErrorPaymentScheduleInvalid": "Invalid payment schedule." + "roleInfoErrorPaymentScheduleInvalid": "Invalid payment schedule.", + "cancelPayment": "Cancel Payment" } From ed3d750a108c64fa8be197cef3f6f2a991b47ebe Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:46:14 -0400 Subject: [PATCH 47/58] remove ability to edit stream --- .../Roles/forms/RoleFormPaymentStreams.tsx | 3 - src/hooks/utils/useCreateRoles.ts | 97 ------------------- 2 files changed, 100 deletions(-) diff --git a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx index 4e9dd62ba..a4dd76599 100644 --- a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx +++ b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx @@ -66,9 +66,6 @@ export function RoleFormPaymentStreams() { isCancelled: payment.isCancelled ?? false, isStreaming: payment.isStreaming ?? (() => false), }} - onClick={() => { - setFieldValue('roleEditing.roleEditingPaymentIndex', index); - }} /> ); diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 65424b2ea..4ffd5fe4f 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -594,103 +594,6 @@ export default function useCreateRoles() { } } if (formHat.editedRole.fieldNames.includes('payments')) { - /** - * Updated Role Payments - * Transfer hat to DAO if there are funds to claim, flush edited active streams with funds to claim, cancel edited streams, transfer hat back to wearer if necessary - */ - - if (!formHat.smartAddress) { - throw new Error('Cannot prepare transactions for edited role without smart address'); - } - const editedStreams = (formHat.payments ?? []).filter(payment => { - if (payment.streamId === undefined) { - return false; - } - const originalPayment = getPayment(formHat.id, payment.streamId); - if (originalPayment === null) { - return false; - } - return ( - !isEqual(payment, originalPayment) && - !payment.isCancelled && - payment.endDate && - payment.endDate > new Date() - ); - }); - const editedStreamsWithFundsToClaim = editedStreams.filter( - stream => (stream?.withdrawableAmount ?? 0n) > 0n, - ); - if (editedStreamsWithFundsToClaim.length) { - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], - }), - targetAddress: hatsProtocol, - }); - for (const stream of editedStreamsWithFundsToClaim) { - if (!stream.streamId || !stream.contractAddress) { - throw new Error( - 'Stream ID and Stream ContractAddress is required for flush stream transaction', - ); - } - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - stream.streamId, - stream.contractAddress, - getAddress(formHat.wearer), - ); - allTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: formHat.smartAddress, - }); - } - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], - }), - targetAddress: hatsProtocol, - }); - } - if (editedStreams.length) { - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], - }), - targetAddress: hatsProtocol, - }); - for (const stream of editedStreams) { - if (!stream.streamId || !stream.contractAddress) { - throw new Error( - 'Stream ID and Stream ContractAddress is required for flush stream transaction', - ); - } - const cancelStreamTx = prepareCancelStreamTx( - stream.streamId, - stream.contractAddress, - ); - allTxs.push(cancelStreamTx); - } - allTxs.push({ - calldata: encodeFunctionData({ - abi: HatsAbi, - functionName: 'transferHat', - args: [BigInt(formHat.id), daoAddress, getAddress(formHat.wearer)], - }), - targetAddress: hatsProtocol, - }); - - const newStreamTxData = createBatchLinearStreamCreationTx( - editedStreams, - formHat.smartAddress, - ); - allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); - allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); - } /** * New Streams * Create new streams From ec8727a0389d26cc7d71ca540bc2ed181cafddbd Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:06:36 -0400 Subject: [PATCH 48/58] remove borderRadius when cancel section is shown --- src/components/pages/Roles/RolePaymentDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 8323a0a78..b63cd4bef 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -298,7 +298,7 @@ export function RolePaymentDetails({ /> )} Date: Mon, 9 Sep 2024 23:23:51 -0400 Subject: [PATCH 49/58] setup to pick up cancelling when it is selected --- .../pages/Roles/RolePaymentDetails.tsx | 34 +++++++++--- .../pages/Roles/RolesDetailsDrawer.tsx | 1 + .../pages/Roles/RolesDetailsDrawerMobile.tsx | 1 + .../Roles/forms/RoleFormPaymentStreams.tsx | 54 ++++++++++--------- .../Roles/forms/useRoleFormEditedRole.tsx | 4 +- src/components/pages/Roles/types.tsx | 1 + 6 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index b63cd4bef..0d361593e 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -1,6 +1,7 @@ import { Box, Button, Flex, Grid, GridItem, Icon, IconButton, Image, Text } from '@chakra-ui/react'; import { Calendar, DotsThree, Download, Trash } from '@phosphor-icons/react'; import { format } from 'date-fns'; +import { useFormikContext } from 'formik'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -15,25 +16,36 @@ import { BigIntValuePair } from '../../../types'; import { DEFAULT_DATE_FORMAT, formatCoin, formatUSD } from '../../../utils'; import { ModalType } from '../../ui/modals/ModalProvider'; import { useDecentModal } from '../../ui/modals/useDecentModal'; +import { RoleFormValues } from './types'; const PAYMENT_DETAILS_BOX_SHADOW = '0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset'; function CancelStreamMenu({ + paymentIndex, + paymentIsCancelling, onSuccess, - // hatId, - // streamId, }: { - hatId: Hex; - streamId: string; + paymentIndex: number; + paymentIsCancelling?: boolean; onSuccess: () => void; }) { + const { values, setFieldValue } = useFormikContext(); const { t } = useTranslation(['roles']); const [showMenu, setShowMenu] = useState(false); const menuRef = useRef(null); const handleCancelPayment = () => { + const payment = values.roleEditing?.payments?.[paymentIndex]; + if (!payment) { + throw new Error('Payment not found'); + } + setFieldValue(`roleEditing.payments.${paymentIndex}`, { + ...payment, + isCancelling: true, + }); setTimeout(() => onSuccess(), 50); + setShowMenu(false); }; useEffect(() => { @@ -48,6 +60,10 @@ function CancelStreamMenu({ }; }, []); + if (paymentIsCancelling) { + return null; + } + return ( boolean; withdrawableAmount?: bigint; }; @@ -178,6 +196,7 @@ export function RolePaymentDetails({ roleHatSmartAddress, roleHatId, showCancel, + paymentIndex, }: RolePaymentDetailsProps) { const { t } = useTranslation(['roles']); const { @@ -258,7 +277,8 @@ export function RolePaymentDetails({ return Number(payment.amount.value) * foundAsset.usdPrice; }, [payment, assetsFungible]); - const isActiveStream = !payment.isCancelled && Date.now() < payment.endDate.getTime(); + const isActiveStream = + !payment.isCancelled && Date.now() < payment.endDate.getTime() && !payment.isCancelling; const activeStreamProps = useCallback( (isTop: boolean) => @@ -292,8 +312,8 @@ export function RolePaymentDetails({ > {showCancel && !!roleHatId && !!payment.streamId && ( {}} /> )} diff --git a/src/components/pages/Roles/RolesDetailsDrawer.tsx b/src/components/pages/Roles/RolesDetailsDrawer.tsx index fb353eaba..805be2389 100644 --- a/src/components/pages/Roles/RolesDetailsDrawer.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawer.tsx @@ -178,6 +178,7 @@ export default function RolesDetailsDrawer({ .sort(paymentSorterByActiveStatus) .map((payment, index) => ( ( { pushPayment({ isStreaming: () => false, + isCancelling: false, }); await validateForm(); setFieldValue('roleEditing.roleEditingPaymentIndex', (payments ?? []).length); @@ -43,31 +44,32 @@ export function RoleFormPaymentStreams() { if (!payment.amount || !payment.asset || !payment.startDate || !payment.endDate) return null; return ( - - Date.now() - } - payment={{ - streamId: payment.streamId, - amount: payment.amount, - asset: payment.asset, - endDate: payment.endDate, - startDate: payment.startDate, - cliffDate: payment.cliffDate, - isCancelled: payment.isCancelled ?? false, - isStreaming: payment.isStreaming ?? (() => false), - }} - /> - + Date.now() + } + payment={{ + streamId: payment.streamId, + amount: payment.amount, + asset: payment.asset, + endDate: payment.endDate, + startDate: payment.startDate, + cliffDate: payment.cliffDate, + isCancelled: payment.isCancelled ?? false, + isStreaming: payment.isStreaming ?? (() => false), + isCancelling: payment.isCancelling, + }} + /> ); })} diff --git a/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx b/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx index 137da6269..e43290823 100644 --- a/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx +++ b/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx @@ -48,6 +48,7 @@ export function useRoleFormEditedRole({ hatsTree }: { hatsTree: DecentTree | und const hasStartDateChanged = payment.startDate?.getTime() !== existingPayment.startDate.getTime(); const hasEndDateChanged = payment.endDate?.getTime() !== existingPayment.endDate.getTime(); + const hasBeenSetToCancel = payment.isCancelling return ( hasAddedCliff || @@ -56,7 +57,8 @@ export function useRoleFormEditedRole({ hatsTree }: { hatsTree: DecentTree | und hasAmountChanged || hasAssetChanged || hasStartDateChanged || - hasEndDateChanged + hasEndDateChanged || + hasBeenSetToCancel ); }); }, [existingRoleHat, values.roleEditing]); diff --git a/src/components/pages/Roles/types.tsx b/src/components/pages/Roles/types.tsx index 7c2eeb828..bd072a178 100644 --- a/src/components/pages/Roles/types.tsx +++ b/src/components/pages/Roles/types.tsx @@ -28,6 +28,7 @@ export interface SablierPayment extends BaseSablierStream { export interface SablierPaymentFormValues extends Partial { isStreaming: () => boolean; + isCancelling?: boolean; } export interface RoleProps { From 2ad7309f01b84b10c5805eddea629e93354d113c Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:32:43 -0400 Subject: [PATCH 50/58] update editedRole logic to remove 'edit' role logic and only capture new and canceled payments --- .../Roles/forms/useRoleFormEditedRole.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx b/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx index e43290823..3bc6fa24f 100644 --- a/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx +++ b/src/components/pages/Roles/forms/useRoleFormEditedRole.tsx @@ -32,36 +32,11 @@ export function useRoleFormEditedRole({ hatsTree }: { hatsTree: DecentTree | und return false; } return values.roleEditing.payments.some(payment => { - const existingPayment = existingRoleHat?.payments?.find(p => p.streamId === payment.streamId); - if (!existingPayment) { - return true; - } - - const hasAddedCliff = !!payment.cliffDate || !existingPayment.cliffDate; - const hasRemovedCliff = !payment.cliffDate || !!existingPayment.cliffDate; - const hasCliffChanged = - !!payment.cliffDate && - !!existingPayment.cliffDate && - payment.cliffDate?.getTime() !== existingPayment.cliffDate.getTime(); - const hasAmountChanged = payment.amount !== existingPayment.amount; - const hasAssetChanged = payment.asset?.address !== existingPayment.asset.address; - const hasStartDateChanged = - payment.startDate?.getTime() !== existingPayment.startDate.getTime(); - const hasEndDateChanged = payment.endDate?.getTime() !== existingPayment.endDate.getTime(); - const hasBeenSetToCancel = payment.isCancelling - - return ( - hasAddedCliff || - hasRemovedCliff || - hasCliffChanged || - hasAmountChanged || - hasAssetChanged || - hasStartDateChanged || - hasEndDateChanged || - hasBeenSetToCancel - ); + const hasBeenSetToCancel = payment.isCancelling; + const isNewPayment = !payment.streamId; + return hasBeenSetToCancel || isNewPayment; }); - }, [existingRoleHat, values.roleEditing]); + }, [values.roleEditing]); const editedRoleData = useMemo(() => { if (!existingRoleHat) { From 919e03a3bb96b36d8a87e9a05d60e7a367c1c80c Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 10 Sep 2024 02:04:07 -0400 Subject: [PATCH 51/58] update and remove logic around 'unedited' payments --- src/hooks/utils/useCreateRoles.ts | 58 ++++--------------------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 4ffd5fe4f..090927930 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -1,5 +1,4 @@ import { FormikHelpers } from 'formik'; -import { isEqual } from 'lodash'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -33,7 +32,7 @@ export default function useCreateRoles() { const { node: { safe, daoAddress, daoName }, } = useFractal(); - const { hatsTree, hatsTreeId, getHat, getPayment } = useRolesStore(); + const { hatsTree, hatsTreeId, getHat } = useRolesStore(); const { addressPrefix, chain, @@ -491,7 +490,7 @@ export default function useCreateRoles() { if (formHat.editedRole.fieldNames.includes('member')) { /** * Updated Role Member - * Transfer hat to DAO, flush inactive and unedited streams, transfer hat to new wearer + * Transfer hat to DAO, flush withdrawable streams, transfer hat to new wearer */ if (formHat.smartAddress === undefined) { @@ -501,13 +500,11 @@ export default function useCreateRoles() { if (!originalHat) { throw new Error('Cannot find original hat'); } - const inactiveFundsToClaimStream = (formHat.payments ?? []).filter( - payment => - (payment?.withdrawableAmount ?? 0n) > 0n && - ((!!payment.endDate && payment.endDate > new Date()) || !!payment.isCancelled), + const fundsToClaimStreams = (formHat.payments ?? []).filter( + payment => (payment?.withdrawableAmount ?? 0n) > 0n && !payment.isCancelling, ); - if (inactiveFundsToClaimStream.length) { + if (fundsToClaimStreams.length) { allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -516,7 +513,7 @@ export default function useCreateRoles() { }), targetAddress: hatsProtocol, }); - for (const stream of inactiveFundsToClaimStream) { + for (const stream of fundsToClaimStreams) { if (!stream.streamId || !stream.contractAddress) { throw new Error( 'Stream ID and Stream ContractAddress is required for flush stream transaction', @@ -532,47 +529,7 @@ export default function useCreateRoles() { targetAddress: formHat.smartAddress, }); } - } - - const unEditedActiveStreams = (formHat.payments ?? []).filter(payment => { - if (payment.streamId === undefined || payment.endDate === undefined) { - return false; - } - const originalPayment = getPayment(formHat.id, payment.streamId); - if (originalPayment === null) { - return false; - } - return ( - isEqual(payment, originalPayment) && - !payment.isCancelled && - payment.endDate < new Date() && - (payment?.withdrawableAmount ?? 0n) > 0n - ); - }); - - if (unEditedActiveStreams.length) { - for (const stream of unEditedActiveStreams) { - if (!stream.streamId || !stream.contractAddress) { - throw new Error( - 'Stream ID and Stream ContractAddress is required for flush stream transaction', - ); - } - const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( - stream.streamId, - stream.contractAddress, - originalHat.wearer, - ); - allTxs.push({ - calldata: wrappedFlushStreamTx, - targetAddress: formHat.smartAddress, - }); - } - } - // transfer hat from DAO to new wearer if there are any funds to claim - if ( - (inactiveFundsToClaimStream && inactiveFundsToClaimStream.length) || - (unEditedActiveStreams && unEditedActiveStreams.length) - ) { + // transfer hat from DAO to new wearer if there are any funds to claim allTxs.push({ calldata: encodeFunctionData({ abi: HatsAbi, @@ -632,7 +589,6 @@ export default function useCreateRoles() { createSmartAccountTx, daoAddress, getHat, - getPayment, hatsDetailsBuilder, hatsProtocol, hatsTree, From d29787446079f0095c470e327dd45e1341b9899e Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 10 Sep 2024 02:11:20 -0400 Subject: [PATCH 52/58] add logic for cancelling stream --- src/hooks/utils/useCreateRoles.ts | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index 090927930..840d8cd37 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -570,6 +570,44 @@ export default function useCreateRoles() { allTxs.push(...newStreamTxData.preparedTokenApprovalsTransactions); allTxs.push(...newStreamTxData.preparedStreamCreationTransactions); } + + /** + * Cancelled Streams + * Transfer hat to DAO, flush withdrawable streams, cancel streams + */ + const cancelledStreams = (formHat.payments ?? []).filter( + payment => payment.isCancelling && !!payment.streamId, + ); + if (cancelledStreams.length) { + for (const stream of cancelledStreams) { + if (!stream.streamId || !stream.contractAddress || !formHat.smartAddress) { + throw new Error('Stream data is missing for cancel stream transaction'); + } + // transfer hat to DAO + allTxs.push({ + calldata: encodeFunctionData({ + abi: HatsAbi, + functionName: 'transferHat', + args: [BigInt(formHat.id), getAddress(formHat.wearer), daoAddress], + }), + targetAddress: hatsProtocol, + }); + // flush withdrawable streams + if (stream.withdrawableAmount && stream.withdrawableAmount > 0n) { + const wrappedFlushStreamTx = prepareHatsAccountFlushExecData( + stream.streamId, + stream.contractAddress, + getAddress(formHat.wearer), + ); + allTxs.push({ + calldata: wrappedFlushStreamTx, + targetAddress: formHat.smartAddress, + }); + } + // cancel stream + allTxs.push(prepareCancelStreamTx(stream.streamId, stream.contractAddress)); + } + } } } else { throw new Error('Invalid Edited Status'); From 278b2008f5723f4f1dce4a9e2eccb3ea525b05c3 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 10 Sep 2024 02:47:35 -0400 Subject: [PATCH 53/58] fix indexing issue; adjust to use streamId to find index --- .../pages/Roles/RolePaymentDetails.tsx | 18 ++++++++++++------ .../pages/Roles/RolesDetailsDrawer.tsx | 1 - .../pages/Roles/RolesDetailsDrawerMobile.tsx | 1 - .../Roles/forms/RoleFormPaymentStreams.tsx | 1 - 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 0d361593e..d01ac2fff 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -22,11 +22,11 @@ const PAYMENT_DETAILS_BOX_SHADOW = '0px 0px 0px 1px #100414, 0px 0px 0px 1px rgba(248, 244, 252, 0.04) inset, 0px 1px 0px 0px rgba(248, 244, 252, 0.04) inset'; function CancelStreamMenu({ - paymentIndex, + streamId, paymentIsCancelling, onSuccess, }: { - paymentIndex: number; + streamId: any; paymentIsCancelling?: boolean; onSuccess: () => void; }) { @@ -36,7 +36,16 @@ function CancelStreamMenu({ const menuRef = useRef(null); const handleCancelPayment = () => { + const paymentIndex = values.roleEditing?.payments?.findIndex( + stream => stream.streamId === streamId, + ); + if (paymentIndex === undefined) { + throw new Error('Payment index not found'); + } const payment = values.roleEditing?.payments?.[paymentIndex]; + if (payment === undefined) { + throw new Error('Payment not found'); + } if (!payment) { throw new Error('Payment not found'); } @@ -86,7 +95,6 @@ function CancelStreamMenu({ position="absolute" ref={menuRef} minW="15.25rem" - top="45%" rounded="0.5rem" bg={NEUTRAL_2_82_TRANSPARENT} border="1px solid" @@ -165,7 +173,6 @@ interface RolePaymentDetailsProps { roleHatId?: Hex; roleHatWearerAddress?: Address; roleHatSmartAddress?: Address; - paymentIndex: number; payment: { streamId?: string; contractAddress?: Address; @@ -196,7 +203,6 @@ export function RolePaymentDetails({ roleHatSmartAddress, roleHatId, showCancel, - paymentIndex, }: RolePaymentDetailsProps) { const { t } = useTranslation(['roles']); const { @@ -312,7 +318,7 @@ export function RolePaymentDetails({ > {showCancel && !!roleHatId && !!payment.streamId && ( {}} /> diff --git a/src/components/pages/Roles/RolesDetailsDrawer.tsx b/src/components/pages/Roles/RolesDetailsDrawer.tsx index 805be2389..fb353eaba 100644 --- a/src/components/pages/Roles/RolesDetailsDrawer.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawer.tsx @@ -178,7 +178,6 @@ export default function RolesDetailsDrawer({ .sort(paymentSorterByActiveStatus) .map((payment, index) => ( ( Date: Tue, 10 Sep 2024 10:03:48 -0400 Subject: [PATCH 54/58] adjust placement of Cancel payment button --- src/components/pages/Roles/RolePaymentDetails.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index d01ac2fff..6a43a3b36 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -96,6 +96,7 @@ function CancelStreamMenu({ ref={menuRef} minW="15.25rem" rounded="0.5rem" + mt="2rem" bg={NEUTRAL_2_82_TRANSPARENT} border="1px solid" borderColor="neutral-3" From a93de75cd649f7027bc7843b85993f3440d9f1ca Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 11 Sep 2024 17:28:24 -0400 Subject: [PATCH 55/58] If PAX, hardcode price to one dollar --- netlify/functions/tokenBalances.mts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/netlify/functions/tokenBalances.mts b/netlify/functions/tokenBalances.mts index 910b1e3db..d4e5540f0 100644 --- a/netlify/functions/tokenBalances.mts +++ b/netlify/functions/tokenBalances.mts @@ -14,16 +14,24 @@ export default async function getTokenBalancesWithPrices(request: Request) { const fetchFromMoralis = async (scope: { chain: string; address: Address }) => { const tokensResponse = await Moralis.EvmApi.wallets.getWalletTokenBalancesPrice(scope); - const mappedTokensData = tokensResponse.result .filter(tokenBalance => tokenBalance.balance.value.toBigInt() > 0n) - .map( - tokenBalance => - ({ - ...camelCaseKeys(tokenBalance.toJSON()), - decimals: Number(tokenBalance.decimals), - }) as unknown as TokenBalance, - ); + .map(tokenBalance => { + const tokenData = { + ...camelCaseKeys(tokenBalance.toJSON()), + } as unknown as TokenBalance; + + if ( + scope.chain === '1' && + tokenData.tokenAddress === '0x8e870d67f660d95d5be530380d0ec0bd388289e1' + ) { + // USDP and just hardcode it to $1 because Moralis is saying (as of Sept 11 2024) that the price is $0 + tokenData.usdPrice = 1; + tokenData.usdValue = Number(tokenData.balanceFormatted) * tokenData.usdPrice; + } + + return tokenData; + }); return mappedTokensData; }; From ef8cd91bf0df682361aa3ac94ac9889aa757001a Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 11 Sep 2024 17:28:51 -0400 Subject: [PATCH 56/58] Don't attempt to getAddress on null objects --- src/hooks/DAO/loaders/useDecentTreasury.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/DAO/loaders/useDecentTreasury.ts b/src/hooks/DAO/loaders/useDecentTreasury.ts index bfb4787c5..d60ba9ca9 100644 --- a/src/hooks/DAO/loaders/useDecentTreasury.ts +++ b/src/hooks/DAO/loaders/useDecentTreasury.ts @@ -109,8 +109,8 @@ export const useDecentTreasury = () => { const tokenAddresses = transfers.results // map down to just the addresses, with a type of `string | undefined` .map(transfer => transfer.tokenAddress) - // no undefined addresses - .filter(address => address !== undefined) + // no undefined or null addresses + .filter(address => address !== undefined && address !== null) // make unique .filter((value, index, self) => self.indexOf(value) === index) // turn them into Address type From 974e7b97ba91ac81c0aec4857cc776b014aafadb Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:59:46 -0400 Subject: [PATCH 57/58] feedback comments --- .../pages/Roles/RolePaymentDetails.tsx | 6 +- .../pages/Roles/RolesDetailsDrawer.tsx | 34 +++++--- .../pages/Roles/RolesDetailsDrawerMobile.tsx | 34 +++++--- .../Roles/forms/RoleFormPaymentStreams.tsx | 80 ++++++++++--------- 4 files changed, 88 insertions(+), 66 deletions(-) diff --git a/src/components/pages/Roles/RolePaymentDetails.tsx b/src/components/pages/Roles/RolePaymentDetails.tsx index 6a43a3b36..c338538f8 100644 --- a/src/components/pages/Roles/RolePaymentDetails.tsx +++ b/src/components/pages/Roles/RolePaymentDetails.tsx @@ -291,7 +291,7 @@ export function RolePaymentDetails({ (isTop: boolean) => isActiveStream ? { - bg: '#221D25', + bg: 'neutral-2', sx: undefined, boxShadow: PAYMENT_DETAILS_BOX_SHADOW, } @@ -303,10 +303,8 @@ export function RolePaymentDetails({ }, bg: 'none', boxShadow: 'none', - borderTop: '1px solid', + border: '1px solid', borderBottom: isTop ? 'none' : '1px solid', - borderLeft: '1px solid', - borderRight: '1px solid', borderColor: 'neutral-4', }, [isActiveStream], diff --git a/src/components/pages/Roles/RolesDetailsDrawer.tsx b/src/components/pages/Roles/RolesDetailsDrawer.tsx index fb353eaba..9c1ca4d92 100644 --- a/src/components/pages/Roles/RolesDetailsDrawer.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawer.tsx @@ -10,6 +10,7 @@ import { Text, } from '@chakra-ui/react'; import { List, PencilLine, User, X } from '@phosphor-icons/react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Hex, getAddress } from 'viem'; import { isFeatureEnabled } from '../../../constants/common'; @@ -69,6 +70,17 @@ export default function RolesDetailsDrawer({ }); const avatarURL = useAvatar(roleHat.wearer); + const sortedPayments = useMemo( + () => + roleHat.payments + ? [...roleHat.payments] + .sort(paymentSorterByWithdrawAmount) + .sort(paymentSorterByStartDate) + .sort(paymentSorterByActiveStatus) + : [], + [roleHat.payments], + ); + if (!daoAddress) return null; return ( @@ -172,19 +184,15 @@ export default function RolesDetailsDrawer({ > {t('payments')} - {[...roleHat.payments] - .sort(paymentSorterByWithdrawAmount) - .sort(paymentSorterByStartDate) - .sort(paymentSorterByActiveStatus) - .map((payment, index) => ( - - ))} + {sortedPayments.map((payment, index) => ( + + ))} )} diff --git a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx index a768b7252..d9e3c95d4 100644 --- a/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx +++ b/src/components/pages/Roles/RolesDetailsDrawerMobile.tsx @@ -1,5 +1,6 @@ import { Box, Flex, Icon, IconButton, Text } from '@chakra-ui/react'; import { PencilLine } from '@phosphor-icons/react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getAddress, Hex } from 'viem'; import { isFeatureEnabled } from '../../../constants/common'; @@ -37,6 +38,17 @@ export default function RolesDetailsDrawerMobile({ const { t } = useTranslation('roles'); const { hatsTree } = useRolesStore(); + const sortedPayments = useMemo( + () => + roleHat.payments + ? [...roleHat.payments] + .sort(paymentSorterByWithdrawAmount) + .sort(paymentSorterByStartDate) + .sort(paymentSorterByActiveStatus) + : [], + [roleHat.payments], + ); + if (!daoAddress || !hatsTree) return null; return ( @@ -105,19 +117,15 @@ export default function RolesDetailsDrawerMobile({ > {t('payments')} - {[...roleHat.payments] - .sort(paymentSorterByWithdrawAmount) - .sort(paymentSorterByStartDate) - .sort(paymentSorterByActiveStatus) - .map((payment, index) => ( - - ))} + {sortedPayments.map((payment, index) => ( + + ))} )} diff --git a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx index 607f7b1de..f8694afa6 100644 --- a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx +++ b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx @@ -1,6 +1,7 @@ import { Box, Button } from '@chakra-ui/react'; import { Plus } from '@phosphor-icons/react'; import { FieldArray, useFormikContext } from 'formik'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { paymentSorterByWithdrawAmount, @@ -15,6 +16,17 @@ export function RoleFormPaymentStreams() { const { values, setFieldValue, validateForm } = useFormikContext(); const payments = values.roleEditing?.payments; + const sortedPayments = useMemo( + () => + payments + ? [...payments] + .sort(paymentSorterByWithdrawAmount) + .sort(paymentSorterByStartDate) + .sort(paymentSorterByActiveStatus) + : [], + [payments], + ); + return ( {({ push: pushPayment }: { push: (streamFormValue: SablierPaymentFormValues) => void }) => ( @@ -35,42 +47,38 @@ export function RoleFormPaymentStreams() { {t('addPayment')} - {[...(payments ?? [])] - .sort(paymentSorterByWithdrawAmount) - .sort(paymentSorterByStartDate) - .sort(paymentSorterByActiveStatus) - .map((payment, index) => { - // @note don't render if form isn't valid - if (!payment.amount || !payment.asset || !payment.startDate || !payment.endDate) - return null; - return ( - Date.now() - } - payment={{ - streamId: payment.streamId, - amount: payment.amount, - asset: payment.asset, - endDate: payment.endDate, - startDate: payment.startDate, - cliffDate: payment.cliffDate, - isCancelled: payment.isCancelled ?? false, - isStreaming: payment.isStreaming ?? (() => false), - isCancelling: payment.isCancelling, - }} - /> - ); - })} + {sortedPayments.map((payment, index) => { + // @note don't render if form isn't valid + if (!payment.amount || !payment.asset || !payment.startDate || !payment.endDate) + return null; + return ( + Date.now() + } + payment={{ + streamId: payment.streamId, + amount: payment.amount, + asset: payment.asset, + endDate: payment.endDate, + startDate: payment.startDate, + cliffDate: payment.cliffDate, + isCancelled: payment.isCancelled ?? false, + isStreaming: payment.isStreaming ?? (() => false), + isCancelling: payment.isCancelling, + }} + /> + ); + })} )} From 6f4857812855e08445afdcbb9e764b2a43e67263 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 12 Sep 2024 14:20:32 -0400 Subject: [PATCH 58/58] Bump version to 0.3.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3c33a984..25bd3e56c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "decent-interface", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decent-interface", - "version": "0.3.1", + "version": "0.3.2", "hasInstallScript": true, "dependencies": { "@amplitude/analytics-browser": "^2.11.1", diff --git a/package.json b/package.json index 681b6e9be..2d4590440 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "decent-interface", - "version": "0.3.1", + "version": "0.3.2", "private": true, "dependencies": { "@amplitude/analytics-browser": "^2.11.1",