From 7feff77c0ae5fab729051979f99c1d07ba9b7cfd Mon Sep 17 00:00:00 2001 From: yu23ki14 Date: Sun, 29 Dec 2024 22:13:05 +0900 Subject: [PATCH] split list --- .../app/components/common/CommonButton.tsx | 1 - .../components/splits/SplitRecipientsList.tsx | 52 +++++++ ...assign.tsx => $treeId_.$hatId_.assign.tsx} | 0 .../app/routes/$treeId_.splits._index.tsx | 94 ++++++------ .../app/routes/$treeId_.splits.new.tsx | 136 +++++++++--------- pkgs/frontend/hooks/useENS.ts | 32 +++-- pkgs/frontend/hooks/useSplitsCreator.ts | 15 +- 7 files changed, 200 insertions(+), 130 deletions(-) create mode 100644 pkgs/frontend/app/components/splits/SplitRecipientsList.tsx rename pkgs/frontend/app/routes/{$treeId_.roles.$hatId_.assign.tsx => $treeId_.$hatId_.assign.tsx} (100%) diff --git a/pkgs/frontend/app/components/common/CommonButton.tsx b/pkgs/frontend/app/components/common/CommonButton.tsx index 3861ad7..cdc8ba9 100644 --- a/pkgs/frontend/app/components/common/CommonButton.tsx +++ b/pkgs/frontend/app/components/common/CommonButton.tsx @@ -3,7 +3,6 @@ import { Button, ButtonProps } from "~/components/ui/button"; interface CommonButtonProps extends Omit { children: React.ReactNode; width?: "full" | number; - size?: "sm" | "md" | "lg"; backgroundColor?: string; color?: string; } diff --git a/pkgs/frontend/app/components/splits/SplitRecipientsList.tsx b/pkgs/frontend/app/components/splits/SplitRecipientsList.tsx new file mode 100644 index 0000000..92883e3 --- /dev/null +++ b/pkgs/frontend/app/components/splits/SplitRecipientsList.tsx @@ -0,0 +1,52 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { FC, useMemo } from "react"; +import { UserIcon } from "../icon/UserIcon"; +import { ipfs2https } from "utils/ipfs"; +import { abbreviateAddress } from "utils/wallet"; + +interface SplitRecipientsListProps { + recipients: { address: string; percentAllocation: number }[]; +} + +export const SplitRecipientsList: FC = ({ + recipients, +}) => { + const addresses = useMemo(() => { + return recipients.map((r) => r.address); + }, [recipients]); + const { names } = useNamesByAddresses(addresses); + + const totalAllocation = useMemo(() => { + return recipients.reduce((acc, r) => acc + r.percentAllocation, 0); + }, [recipients]); + + return ( + <> + {recipients.map((recipient) => { + const name = names.find( + (name) => name[0]?.address === recipient.address + )?.[0]; + return ( + + + + {name?.name} + + {abbreviateAddress(name?.address || "")} + + + {( + (Number(recipient.percentAllocation) / totalAllocation) * + 100 + ).toFixed(2)}{" "} + % + + ); + })} + + ); +}; diff --git a/pkgs/frontend/app/routes/$treeId_.roles.$hatId_.assign.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.assign.tsx similarity index 100% rename from pkgs/frontend/app/routes/$treeId_.roles.$hatId_.assign.tsx rename to pkgs/frontend/app/routes/$treeId_.$hatId_.assign.tsx diff --git a/pkgs/frontend/app/routes/$treeId_.splits._index.tsx b/pkgs/frontend/app/routes/$treeId_.splits._index.tsx index 3fbf645..7101fa2 100644 --- a/pkgs/frontend/app/routes/$treeId_.splits._index.tsx +++ b/pkgs/frontend/app/routes/$treeId_.splits._index.tsx @@ -4,17 +4,15 @@ import { Link, useParams } from "@remix-run/react"; import { FC, useCallback, useState } from "react"; import { CommonButton } from "~/components/common/CommonButton"; import { FaAngleDown, FaRegCopy } from "react-icons/fa6"; -import { UserIcon } from "~/components/icon/UserIcon"; import { useCopyToClipboard } from "hooks/useCopyToClipboard"; import { useGetWorkspace } from "hooks/useWorkspace"; import { useSplitsCreatorRelatedSplits } from "hooks/useSplitsCreator"; import { Address } from "viem"; import { Split } from "@0xsplits/splits-sdk"; import { abbreviateAddress } from "utils/wallet"; -import { publicClient } from "hooks/useViem"; +import { currentChain, publicClient } from "hooks/useViem"; import dayjs from "dayjs"; -import { useNamesByAddresses } from "hooks/useENS"; -import { ipfs2https } from "utils/ipfs"; +import { SplitRecipientsList } from "~/components/splits/SplitRecipientsList"; interface SplitInfoItemProps { split: Split; @@ -23,10 +21,22 @@ interface SplitInfoItemProps { const SplitInfoItem: FC = ({ split }) => { const [createdTime, setCreatedTime] = useState(); - const addresses = useMemo(() => { - return split.recipients.map((r) => r.recipient.address); - }, [split]); - const { names } = useNamesByAddresses(addresses); + const consolidatedRecipients = useMemo(() => { + const consolidated = split.recipients.reduce( + (acc, recipient) => { + const address = recipient.recipient.address; + acc[address] = + (acc[address] || 0) + Number(recipient.percentAllocation); + return acc; + }, + {} as Record + ); + + return Object.entries(consolidated).map(([address, percentAllocation]) => ({ + address, + percentAllocation, + })); + }, [split.recipients]); const [open, setOpen] = useState(false); const onOpen = useCallback(() => { @@ -43,12 +53,6 @@ const SplitInfoItem: FC = ({ split }) => { [copyToClipboardAction] ); - const totalAllocation = useMemo(() => { - return split.recipients.reduce((acc, recipient) => { - return acc + Number(recipient.percentAllocation); - }, 0); - }, [split]); - useEffect(() => { const fetch = async () => { const data = await publicClient.getBlock({ @@ -63,10 +67,27 @@ const SplitInfoItem: FC = ({ split }) => { }, [split]); return ( - + - {abbreviateAddress(split.address)} + + {abbreviateAddress(split.address)} + + + 詳細を確認 + + + Created at {createdTime} @@ -106,35 +127,7 @@ const SplitInfoItem: FC = ({ split }) => { borderTop="1px solid #868e96" role="presentation" > - {names.map((name) => ( - - - - {name[0]?.name} - - {abbreviateAddress(name[0]?.address || "")} - - - {(Number( - split.recipients.find( - (recipient) => - recipient.recipient.address.toLowerCase() === - name[0]?.address.toLowerCase() - )?.percentAllocation - ) / - totalAllocation) * - 100}{" "} - % - - ))} + @@ -169,10 +162,13 @@ const SplitsIndex: FC = () => { {isLoading ? ( <> ) : ( - - {splits.map((split) => ( - - ))} + + {splits + .slice() + .sort((a, b) => Number(b.createdBlock) - Number(a.createdBlock)) + .map((split) => ( + + ))} )} diff --git a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx index a2bd49b..e210b97 100644 --- a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx +++ b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx @@ -10,7 +10,7 @@ import { VStack, } from "@chakra-ui/react"; import { Hat, Wearer } from "@hatsprotocol/sdk-v1-subgraph"; -import { useParams } from "@remix-run/react"; +import { useNavigate, useParams } from "@remix-run/react"; import { useNamesByAddresses } from "hooks/useENS"; import { useAssignableHats, useHats } from "hooks/useHats"; import { useSplitsCreator } from "hooks/useSplitsCreator"; @@ -40,6 +40,7 @@ import { UseFieldArrayUpdate, useForm, } from "react-hook-form"; +import { SplitRecipientsList } from "~/components/splits/SplitRecipientsList"; interface RoleItemProps { update: UseFieldArrayUpdate; @@ -47,6 +48,7 @@ interface RoleItemProps { detail?: HatsDetailSchama; imageUri?: string; field: FieldArrayWithId; + wearers: Wearer[]; } const RoleItem: FC = ({ @@ -55,21 +57,12 @@ const RoleItem: FC = ({ update, fieldIndex, field, + wearers, }) => { - const { getWearersInfo } = useHats(); - - const [wearersAddress, setWearersAddress] = useState([]); - - useEffect(() => { - const fetch = async () => { - const res = await getWearersInfo({ hatId: field.hatId }); - if (!res) return; - setWearersAddress(res.map((w) => w.id)); - }; - fetch(); - }, [field.hatId, getWearersInfo]); - - const { names } = useNamesByAddresses(wearersAddress); + const addresses = useMemo(() => { + return wearers.map((w) => w.id); + }, [wearers]); + const { names } = useNamesByAddresses(addresses); const handleOnCheck = (address: Address) => { if (!address) return; @@ -91,7 +84,7 @@ const RoleItem: FC = ({ }; return ( - + { + const navigate = useNavigate(); + const { treeId } = useParams(); const hats = useAssignableHats(Number(treeId)); @@ -213,7 +208,7 @@ const SplitterNew: FC = () => { ); }, [hats]); - const { createSplits, previewSplits } = useSplitsCreator(treeId!); + const { createSplits, previewSplits, isLoading } = useSplitsCreator(treeId!); const { control, getValues } = useForm(); const { fields, insert, update } = useFieldArray({ @@ -237,57 +232,62 @@ const SplitterNew: FC = () => { } }; fetch(); - }, [baseHats, fields]); + }, [baseHats, fields.length]); const [preview, setPreview] = - useState<{ address: Address; ratio: string }[]>(); + useState<{ address: Address; percentAllocation: number }[]>(); - const handlePreview = async () => { + const calcParams = () => { const data = getValues(); - const params = data.roles.map((role) => ({ - hatId: BigInt(role.hatId), - multiplierBottom: role.multiplier - ? BigInt(String(role.multiplier).split(".")[1].length * 10) - : BigInt(1), - multiplierTop: role.multiplier - ? BigInt( - role.multiplier * String(role.multiplier).split(".")[1].length * 10 - ) - : BigInt(1), - wearers: role.wearers, - })); + return data.roles.map((role) => { + const [multiplierTop, multiplierBottom] = role.multiplier + ? String(role.multiplier).includes(".") + ? [ + BigInt( + role.multiplier * + 10 ** String(role.multiplier).split(".")[1].length + ), + BigInt(10 ** String(role.multiplier).split(".")[1].length), + ] + : [BigInt(role.multiplier), BigInt(1)] + : [BigInt(1), BigInt(1)]; + + return { + hatId: BigInt(role.hatId), + multiplierTop, + multiplierBottom, + wearers: role.wearers, + }; + }); + }; + + const handlePreview = async () => { + const params = calcParams(); const res = await previewSplits(params); - const _preview = []; - const sumOfScore = res[1].reduce((acc, cur) => acc + Number(cur), 0); - for (let index = 0; index < res[0].length; index++) { - const address = res[0][index]; - const score = res[1][index]; - const ratio = ((Number(score) / sumOfScore) * 100).toFixed(2); - _preview.push({ address, ratio }); - } - setPreview(_preview); + const consolidatedRecipients = res[0].reduce((acc, address, index) => { + const percentAllocation = Number(res[1][index]); + acc.set(address, (acc.get(address) || 0) + percentAllocation); + return acc; + }, new Map()); + + setPreview( + Array.from(consolidatedRecipients.entries()).map( + ([address, percentAllocation]) => ({ + address, + percentAllocation, + }) + ) + ); }; const handleCreateSplitter = async () => { - const data = getValues(); - const params = data.roles.map((role) => ({ - hatId: BigInt(role.hatId), - multiplierBottom: role.multiplier - ? BigInt(String(role.multiplier).split(".")[1].length * 10) - : BigInt(1), - multiplierTop: role.multiplier - ? BigInt( - role.multiplier * String(role.multiplier).split(".")[1].length * 10 - ) - : BigInt(1), - wearers: role.wearers, - })); - const txHash = await createSplits({ + const params = calcParams(); + await createSplits({ args: params, }); - console.log(txHash); + navigate(`/${treeId}/splits`); }; return ( @@ -305,17 +305,10 @@ const SplitterNew: FC = () => { {preview ? ( <> - - {preview.map((item, index) => ( - - - {item.address} - {item.ratio}% - - - ))} - - 作成 + + + 作成 + ) : ( <> @@ -333,7 +326,14 @@ const SplitterNew: FC = () => { detailUri={field.hat.details} imageUri={field.hat.imageUri} > - + h.id == field.hatId)?.wearers || [] + } + /> ))} diff --git a/pkgs/frontend/hooks/useENS.ts b/pkgs/frontend/hooks/useENS.ts index 40d21d7..4fee560 100644 --- a/pkgs/frontend/hooks/useENS.ts +++ b/pkgs/frontend/hooks/useENS.ts @@ -29,11 +29,24 @@ export const useNamesByAddresses = (addresses?: string[]) => { const fetchNames = async (addresses: string[]) => { try { - const { data } = await axios.get("/api/namestone/resolve-names", { - params: { addresses: addresses.join(",") }, - }); - setNames(data); - return data as NameData[][]; + const { data } = await axios.get( + "/api/namestone/resolve-names", + { + params: { addresses: addresses.join(",") }, + } + ); + const unresolvedAddresses = addresses + .filter((address) => { + return !data.some( + (nameData) => + nameData[0]?.address.toLowerCase() === address.toLowerCase() + ); + }) + .map((address) => { + return [{ address, name: "", domain: "" }]; + }); + setNames([...data, ...unresolvedAddresses]); + return data; } catch (error) { console.error(error); } @@ -47,9 +60,12 @@ export const useAddressesByNames = (names?: string[], exactMatch?: boolean) => { const fetchAddresses = useCallback(async (resolveNames: string[]) => { try { - const { data } = await axios.get("/api/namestone/resolve-addresses", { - params: { names: resolveNames.join(","), exact_match: exactMatch }, - }); + const { data } = await axios.get( + "/api/namestone/resolve-addresses", + { + params: { names: resolveNames.join(","), exact_match: exactMatch }, + } + ); setAddresses(data); return data as NameData[][]; } catch (error) { diff --git a/pkgs/frontend/hooks/useSplitsCreator.ts b/pkgs/frontend/hooks/useSplitsCreator.ts index ec7901d..6c81fea 100644 --- a/pkgs/frontend/hooks/useSplitsCreator.ts +++ b/pkgs/frontend/hooks/useSplitsCreator.ts @@ -1,7 +1,7 @@ import { SPLITS_CREATOR_ABI } from "abi/splits"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { AbiItemArgs, Address, encodeFunctionData } from "viem"; -import { useActiveWallet, useSmartAccountClient } from "./useWallet"; +import { AbiItemArgs, Address, parseEventLogs } from "viem"; +import { useActiveWallet } from "./useWallet"; import { currentChain, publicClient } from "./useViem"; import { useGetWorkspace } from "./useWorkspace"; import { splitsDataClient } from "utils/splits"; @@ -52,13 +52,20 @@ export const useSplitsCreator = (treeId: string) => { args: [params.args], }); + console.log("txHash:", txHash); + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); - console.log({ receipt }); + const parsedLog = parseEventLogs({ + abi: SPLITS_CREATOR_ABI, + eventName: "SplitsCreated", + logs: receipt.logs, + strict: false, + }); - return txHash; + return parsedLog; } catch (error) { console.error("error occured when creating Splits:", error); } finally {