diff --git a/pkgs/frontend/app/components/PageHeader.tsx b/pkgs/frontend/app/components/PageHeader.tsx index 96d0e37..c996b88 100644 --- a/pkgs/frontend/app/components/PageHeader.tsx +++ b/pkgs/frontend/app/components/PageHeader.tsx @@ -1,4 +1,4 @@ -import { HStack, IconButton, Text } from "@chakra-ui/react"; +import { Box, HStack, IconButton } from "@chakra-ui/react"; import { useNavigate } from "@remix-run/react"; import { type ReactNode, useCallback } from "react"; import { FaChevronLeft } from "react-icons/fa6"; @@ -31,7 +31,7 @@ export const PageHeader: React.FC = ({ title, backLink }) => { > - {title} + {title} ); }; diff --git a/pkgs/frontend/app/components/common/CommonIcon.tsx b/pkgs/frontend/app/components/common/CommonIcon.tsx index c22970c..01a78de 100644 --- a/pkgs/frontend/app/components/common/CommonIcon.tsx +++ b/pkgs/frontend/app/components/common/CommonIcon.tsx @@ -22,6 +22,7 @@ export const CommonIcon = ({ return ( { = ({ detail, treeId }) => ( + + No name + + ) + } + /> +); + +export const RoleNameWithWearer: FC = ({ + detail, + wearerId, + wearerName, + wearerIcon, +}) => ( + + + {detail.data.name} + + + {wearerName ? wearerName : abbreviateAddress(wearerId || "")} + + ) : ( + + No name + + ) + } + /> +); + +export const ActiveState: FC = ({ + isActive, + woreTime, + wearingElapsedTime, +}) => { + const formattedWoreTime = useMemo(() => { + if (!woreTime) return; + return dayjs.unix(woreTime).format("YYYY/MM/DD"); + }, [woreTime]); + + const formattedWearingElapsedTime = useMemo(() => { + if (!wearingElapsedTime) return; + return Math.floor(wearingElapsedTime / 86400); + }, [wearingElapsedTime]); + + return ( + + {isActive ? ( + + Active + + ) : ( + + Inactive + + )} + {formattedWoreTime && formattedWearingElapsedTime && ( + + {isActive && ( + + Activated on{" "} + + {formattedWoreTime} + + + )} + + Active in{" "} + + {formattedWearingElapsedTime}days + + + + )} + + ); +}; + +export const HatDetail: FC = ({ detail, imageUri }) => ( + + + + + + + + 説明 + + + {detail?.data.description ? ( + detail.data.description + ) : ( + + No responsibilities + + )} + {detail?.data.description} + + + + + 役割 + {(detail?.data.responsabilities?.length ?? 0 > 0) ? ( + + {detail?.data.responsabilities?.map((r) => ( + + {r.label} + {r.description} + {r.link && ( + + + リンク + + + )} + + ))} + + ) : ( + + No responsibilities + + )} + + + + 権限 + {(detail?.data.authorities?.length ?? 0 > 0) ? ( + + {detail?.data.authorities?.map((a) => ( + + {a.label} + {a.description} + {a.link && ( + + + リンク + + + )} + + ))} + + ) : ( + + No authorities + + )} + + + + +); diff --git a/pkgs/frontend/app/components/roles/RoleTag.tsx b/pkgs/frontend/app/components/roles/RoleTag.tsx index 90279c1..2a0301b 100644 --- a/pkgs/frontend/app/components/roles/RoleTag.tsx +++ b/pkgs/frontend/app/components/roles/RoleTag.tsx @@ -15,9 +15,9 @@ export const RoleTag: FC = ({ bgColor = "yellow.400", }) => { return ( - + - + {detail?.data?.name} diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx new file mode 100644 index 0000000..4003ba0 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx @@ -0,0 +1,155 @@ +import { Box, Heading, HStack, Text, VStack } from "@chakra-ui/react"; +import { Link, useNavigate, useParams } from "@remix-run/react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { useHoldersWithoutWearers } from "hooks/useFractionToken"; +import { useTreeInfo } from "hooks/useHats"; +import { useWearingElapsedTime } from "hooks/useHatsTimeFrameModule"; +import { useGetWorkspace } from "hooks/useWorkspace"; +import { FC, useMemo } from "react"; +import { ipfs2https } from "utils/ipfs"; +import { abbreviateAddress } from "utils/wallet"; +import { Address } from "viem"; +import { BasicButton } from "~/components/BasicButton"; +import { HatsListItemParser } from "~/components/common/HatsListItemParser"; +import { UserIcon } from "~/components/icon/UserIcon"; +import { HatDetail, RoleName } from "~/components/roles/HolderDetail"; +import { StickyNav } from "~/components/StickyNav"; + +const RoleDetails: FC = () => { + const { treeId, hatId } = useParams(); + + const tree = useTreeInfo(Number(treeId)); + + const hat = useMemo(() => { + if (!tree || !tree.hats) return; + return tree.hats.find((h) => h.id === hatId); + }, [tree, hatId]); + + const wearers = useMemo(() => hat?.wearers, [hat]); + const wearerIds = useMemo( + () => wearers?.map(({ id }) => id.toLowerCase()) || [], + [wearers] + ); + + // wearer + const { names: wearerNames } = useNamesByAddresses(wearerIds); + + // wearerを除くholder + const holdersWithoutWearers = useHoldersWithoutWearers({ + hatId, + wearers: wearerIds, + }); + const assistantMembers = useMemo( + () => + holdersWithoutWearers.filter((h) => !wearerIds.includes(h.toLowerCase())), + [holdersWithoutWearers, wearerIds] + ); + const { names: holderNames } = useNamesByAddresses(assistantMembers); + + // 各wearerのWearingElapsedTimeを取得 + const { data } = useGetWorkspace(treeId); + const hatsTimeFrameModuleAddress = useMemo( + () => data?.workspace?.hatsTimeFrameModule, + [data] + ); + const timeList = useWearingElapsedTime( + hatsTimeFrameModuleAddress as Address, + hatId, + wearerIds + ); + + const navigate = useNavigate(); + + if (!hat) return; + + return ( + + + + + + + + 貢献メンバー + + + + 役割をわたす + + + + + + {wearerNames.flat().map((n, idx) => ( + navigate(`/${treeId}/${hatId}/${n.address}`)} + > + + + + {n.name + ? `${n.name} (${abbreviateAddress(n.address)})` + : abbreviateAddress(n.address)} + + + {Math.floor( + (timeList.find( + ({ wearer }) => + wearer.toLowerCase() === n.address.toLowerCase() + )?.time || 0) / 86400 + )}{" "} + days + + + + 役割保持者 + + + ))} + + {holderNames.flat().map((n, idx) => ( + + + + {n.name + ? `${n.name} (${abbreviateAddress(n.address)})` + : abbreviateAddress(n.address)} + + + Assist + + + ))} + + + + + ); +}; + +export default RoleDetails; diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx new file mode 100644 index 0000000..cb36853 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx @@ -0,0 +1,222 @@ +import { Box, HStack, VStack, Text, Heading } from "@chakra-ui/react"; +import { Link, useNavigate, useParams } from "@remix-run/react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { useHoldersWithBalance } from "hooks/useFractionToken"; +import { useHats, useTreeInfo } from "hooks/useHats"; +import { + useActiveState, + useDeactivate, + useReactivate, +} from "hooks/useHatsTimeFrameModule"; +import { useActiveWallet } from "hooks/useWallet"; +import { useGetWorkspace } from "hooks/useWorkspace"; +import { FC, useMemo, useState } from "react"; +import { ipfs2https } from "utils/ipfs"; +import { abbreviateAddress } from "utils/wallet"; +import { BasicButton } from "~/components/BasicButton"; +import { HatsListItemParser } from "~/components/common/HatsListItemParser"; +import { UserIcon } from "~/components/icon/UserIcon"; +import { + ActiveState, + HatDetail, + RoleNameWithWearer, +} from "~/components/roles/HolderDetail"; +import { StickyNav } from "~/components/StickyNav"; + +const RoleHolderDetails: FC = () => { + const { treeId, hatId, address } = useParams(); + + const { wallet } = useActiveWallet(); + const me = wallet?.account?.address; + + const tree = useTreeInfo(Number(treeId)); + + const hat = useMemo(() => { + if (!tree || !tree.hats) return; + return tree.hats.find((h) => h.id === hatId); + }, [tree, hatId]); + + // ログインユーザーがこのhatの上位のhatのholderであるか + const isAuthorised = useMemo(() => { + if (!me || !hat?.levelAtLocalTree) return false; + + if (hat.wearers?.some((w) => w.id.toLowerCase() === me.toLowerCase())) + return true; + + for (let i = 0; i < hat.levelAtLocalTree; i++) { + const parentHatId = hat.id.slice(0, 10 + 4 * i) + "0".repeat(56 - 4 * i); + + if ( + tree?.hats + ?.find((h) => h.id === parentHatId) + ?.wearers?.some((w) => w.id.toLowerCase() === me.toLowerCase()) + ) + return true; + } + + return false; + }, [me, tree, hat]); + + // wearerの名前とアイコンを取得 + const addresses = useMemo(() => (address ? [address] : undefined), [address]); + const { names: wearerNames } = useNamesByAddresses(addresses); + const { wearerName, wearerIcon } = useMemo( + () => + wearerNames.flat().length > 0 + ? { + wearerName: wearerNames.flat()[0].name, + wearerIcon: wearerNames.flat()[0].text_records?.avatar, + } + : {}, + [wearerNames] + ); + + // holderをbalanceとともに取得 + const holdersWithBalance = useHoldersWithBalance({ wearer: address, hatId }); + const holders = useMemo( + () => holdersWithBalance.map(({ holder }) => holder), + [holdersWithBalance] + ); + const { names: holderNames } = useNamesByAddresses(holders); + const holderDetail = useMemo( + () => + holderNames.flat().map((n) => ({ + ...n, + balance: holdersWithBalance.find( + ({ holder }) => holder.toLowerCase() === n.address.toLowerCase() + )?.balance, + })), + [holdersWithBalance, holderNames] + ); + + // HatsTimeFrameModuleのアドレスを取得 + const { data } = useGetWorkspace(treeId); + const hatsTimeFrameModuleAddress = useMemo( + () => data?.workspace?.hatsTimeFrameModule, + [data] + ); + + // HatsTimeFrameModule関連の情報をボタンクリックの後再取得できるようにカウンターを設置 + const [count, setCount] = useState(0); + const { isActive, woreTime, wearingElapsedTime } = useActiveState( + hatsTimeFrameModuleAddress, + hatId, + address, + count + ); + + // reactivate, deactivate, renounce + const { reactivate, isLoading: isReactivating } = useReactivate( + hatsTimeFrameModuleAddress + ); + const { deactivate, isLoading: isDeactivating } = useDeactivate( + hatsTimeFrameModuleAddress + ); + const { renounceHat, isLoading: isRenouncing } = useHats(); + + const navigate = useNavigate(); + + if (!hat) return; + + return ( + + + + + + + + + アシストクレジット + + + 誰かに送る + + + + + + {holderDetail.length === 0 ? ( + + No holders + + ) : ( + holderDetail.map((h, idx) => ( + + + + {h.name + ? `${h.name} (${abbreviateAddress(h.address)})` + : abbreviateAddress(h.address)} + + {h.balance !== undefined && ( + {Number(h.balance).toLocaleString()} + )} + + )) + )} + + + {/* hatについて権限があるかどうかで表示の有無が変わるボタン */} + {isAuthorised && ( + <> + {isActive ? ( + { + await deactivate(hatId, address); + setCount(count + 1); + }} + disabled={isDeactivating} + > + {isDeactivating ? "Deactivating..." : "Deactivate"} + + ) : ( + { + await reactivate(hatId, address); + setCount(count + 1); + }} + disabled={isReactivating} + > + {isReactivating ? "Reactivating..." : "Reactivate"} + + )} + {/* 現時点では表示されても実際にrevokeできるのはwearerのみ */} + { + await renounceHat(BigInt(hatId || 0)); + navigate(`/${treeId}/${hatId}`); + }} + disabled={isRenouncing} + > + {isRenouncing ? "Revoking..." : "Revoke"} + + + )} + + + + ); +}; + +export default RoleHolderDetails; diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx index de79009..11f33b3 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address_.assistcredit.send.tsx @@ -102,7 +102,7 @@ const AssistCreditSend: FC = () => { return ( { }, [hatId, resolvedAddress, inputValue, mintHat]); return ( - <> - - - - - - - {/* User name or address input */} - - - ユーザー名 or ウォレットアドレス - - setInputValue(e.target.value)} - /> - {resolvedAddress && !isValidEthAddress(inputValue) && ( - - - {abbreviateAddress(resolvedAddress)} - - )} - - - {/* Date input */} - - - 開始日 - - { - setStartDatetime(e.target.value); - }} - type="datetime-local" - /> + + + + + + + + + + {/* User name or address input */} + + + ユーザー名 or ウォレットアドレス + + setInputValue(e.target.value)} + /> + {resolvedAddress && !isValidEthAddress(inputValue) && ( + + + {abbreviateAddress(resolvedAddress)} + + )} + + + {/* Date input */} + + + 開始日 + + { + setStartDatetime(e.target.value); + }} + type="datetime-local" + /> + {/* Assign Button */} @@ -151,7 +156,7 @@ const AssignRole: FC = () => { > Assign - + ); }; diff --git a/pkgs/frontend/app/routes/$treeId_.member.tsx b/pkgs/frontend/app/routes/$treeId_.member.tsx index cd44bf6..7deeb90 100644 --- a/pkgs/frontend/app/routes/$treeId_.member.tsx +++ b/pkgs/frontend/app/routes/$treeId_.member.tsx @@ -115,13 +115,12 @@ const WorkspaceMember: FC = () => { {m.wearer?.hats?.map((h) => ( - + - + ))} diff --git a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx index a09942c..9a41a83 100644 --- a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx +++ b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx @@ -215,7 +215,7 @@ const SplitterNew: FC = () => { const [splitterName, setSplitterName] = useState(""); const _splitterName = useMemo(() => { - return [`${splitterName}.splitter`]; + return [`${splitterName}.split`]; }, [splitterName]); const { addresses } = useAddressesByNames(_splitterName, true); const availableName = useMemo(() => { @@ -256,29 +256,32 @@ const SplitterNew: FC = () => { const calcParams = () => { const data = getValues(); - 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, - }; - }); + return data.roles + .filter((r) => r.active) + .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 handlePreview = useCallback(async () => { + if (!availableName) return; const params = calcParams(); const res = await previewSplits(params); @@ -296,7 +299,7 @@ const SplitterNew: FC = () => { }), ), ); - }; + }, [availableName]); const { setName } = useSetName(); const handleCreateSplitter = async () => { @@ -320,7 +323,7 @@ const SplitterNew: FC = () => { return ( { ))} - + プレビュー diff --git a/pkgs/frontend/app/routes/signup.tsx b/pkgs/frontend/app/routes/signup.tsx index 4223218..f374e64 100644 --- a/pkgs/frontend/app/routes/signup.tsx +++ b/pkgs/frontend/app/routes/signup.tsx @@ -34,7 +34,7 @@ const Login: FC = () => { }, [userName, addresses]); const handleSubmit = async () => { - if (!wallet) return; + if (!wallet || !availableName) return; const params: { name: string; diff --git a/pkgs/frontend/hooks/useContracts.ts b/pkgs/frontend/hooks/useContracts.ts index c3687e4..c19d312 100644 --- a/pkgs/frontend/hooks/useContracts.ts +++ b/pkgs/frontend/hooks/useContracts.ts @@ -15,9 +15,12 @@ export const hatsContractBaseConfig = { abi: HATS_ABI, }; -export const hatsTimeFrameContractBaseConfig = { +export const hatsTimeFrameContractBaseConfig = ( + hatsTimeFrameModuleAddress: Address +) => ({ + address: hatsTimeFrameModuleAddress, abi: HATS_TIME_FRAME_MODULE_ABI, -}; +}); export const fractionTokenBaseConfig = { address: FRACTION_TOKEN_ADDRESS as Address, diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts index cace9db..0c11c96 100644 --- a/pkgs/frontend/hooks/useFractionToken.ts +++ b/pkgs/frontend/hooks/useFractionToken.ts @@ -79,6 +79,97 @@ export const useTokenRecipients = ( return recipients; }; +export const useHoldersWithoutWearers = ({ + hatId, + wearers, +}: { + hatId?: string; + wearers: string[]; +}) => { + const [holders, setHolders] = useState([]); + + const { getTokenId, getTokenRecipients } = useFractionToken(); + + useEffect(() => { + const fetch = async () => { + if (!hatId) return; + try { + const fetchedRecipients = await Promise.all( + wearers.map(async (w) => { + const tokenId = await getTokenId({ + hatId: BigInt(hatId), + account: w as Address, + }); + if (!tokenId) return []; + const recipients = (await getTokenRecipients({ tokenId }))?.filter( + (r) => r.toLowerCase() !== w.toLowerCase() + ); + return recipients || []; + }) + ); + + setHolders(fetchedRecipients.flat()); + } catch (error) { + console.error("error occured when fetching tokenRecipients:", error); + } + }; + + fetch(); + }, [getTokenId, getTokenRecipients, hatId, wearers]); + + return holders; +}; + +export const useHoldersWithBalance = ({ + wearer, + hatId, +}: { + wearer?: string; + hatId?: string; +}) => { + const [holders, setHolders] = useState< + { + holder: Address; + balance: bigint; + }[] + >([]); + + const { getTokenId, getTokenRecipients } = useFractionToken(); + + useEffect(() => { + const fetch = async () => { + if (!wearer || !hatId) return; + try { + const tokenId = await getTokenId({ + hatId: BigInt(hatId), + account: wearer as Address, + }); + if (!tokenId) return; + const holders = [...((await getTokenRecipients({ tokenId })) || [])]; + + const balances = await publicClient.readContract({ + ...fractionTokenBaseConfig, + functionName: "balanceOfBatch", + args: [holders, holders.map(() => BigInt(tokenId))], + }); + + const holdersWithBalance = holders.map((holder, index) => ({ + holder, + balance: balances[index], + })); + + setHolders(holdersWithBalance); + } catch (error) { + console.error("error occured when fetching tokenRecipients:", error); + } + }; + + fetch(); + }, [getTokenId, getTokenRecipients, wearer, hatId]); + + return holders; +}; + export const useBalanceOfFractionToken = ( holder: Address, address: Address, @@ -379,6 +470,8 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => { functionName: "safeTransferFrom", args: [wallet.account.address, to, tokenId, amount, "0x"], }); + } catch (_) { + setIsLoading(false); } finally { setIsLoading(false); } @@ -404,6 +497,7 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => { }); } catch (error) { console.error(error); + setIsLoading(false); } finally { await publicClient.waitForTransactionReceipt({ hash: txHash! }); setIsLoading(false); diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index 0ed6a2c..0633fd9 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -478,6 +478,32 @@ export const useHats = () => { [wallet], ); + const renounceHat = useCallback( + async (hatId: bigint) => { + if (!wallet) return; + + setIsLoading(true); + + try { + const txHash = await wallet.writeContract({ + abi: HATS_ABI, + address: HATS_ADDRESS, + functionName: "renounceHat", + args: [hatId], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [wallet] + ); + return { isLoading, getTreeInfo, @@ -491,6 +517,7 @@ export const useHats = () => { mintHat, changeHatDetails, changeHatImageURI, + renounceHat, }; }; diff --git a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts index dda760f..9a23a21 100644 --- a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts +++ b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts @@ -1,8 +1,8 @@ -import { HATS_TIME_FRAME_MODULE_ABI } from "abi/hatsTimeFrameModule"; -import { useState } from "react"; -import { type Address, parseEventLogs } from "viem"; -import { publicClient } from "./useViem"; import { useActiveWallet } from "./useWallet"; +import { Address } from "viem"; +import { useCallback, useEffect, useState } from "react"; +import { publicClient } from "./useViem"; +import { hatsTimeFrameContractBaseConfig } from "./useContracts"; export const useMintHatFromTimeFrameModule = ( hatsTimeFrameModuleAddress: Address, @@ -18,8 +18,7 @@ export const useMintHatFromTimeFrameModule = ( try { const txHash = await wallet?.writeContract({ - abi: HATS_TIME_FRAME_MODULE_ABI, - address: hatsTimeFrameModuleAddress, + ...hatsTimeFrameContractBaseConfig(hatsTimeFrameModuleAddress), functionName: "mintHat", args: [hatId, wearer, time || BigInt(0)], }); @@ -36,3 +35,175 @@ export const useMintHatFromTimeFrameModule = ( return { mintHat, isLoading }; }; + +export const useReactivate = (hatsTimeFrameModuleAddress?: string) => { + const { wallet } = useActiveWallet(); + + const [isLoading, setIsLoading] = useState(false); + + const reactivate = useCallback( + async (hatId?: string, wearer?: string) => { + if (!hatsTimeFrameModuleAddress || !wallet || !hatId || !wearer) return; + + setIsLoading(true); + + try { + const txHash = await wallet?.writeContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "reactivate", + args: [BigInt(hatId), wearer as Address], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [hatsTimeFrameModuleAddress, wallet] + ); + + return { reactivate, isLoading }; +}; + +export const useDeactivate = (hatsTimeFrameModuleAddress?: string) => { + const { wallet } = useActiveWallet(); + + const [isLoading, setIsLoading] = useState(false); + + const deactivate = useCallback( + async (hatId?: string, wearer?: string) => { + if (!hatsTimeFrameModuleAddress || !wallet || !hatId || !wearer) return; + + setIsLoading(true); + + try { + const txHash = await wallet?.writeContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "deactivate", + args: [BigInt(hatId), wearer as Address], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [hatsTimeFrameModuleAddress, wallet] + ); + + return { deactivate, isLoading }; +}; + +export const useActiveState = ( + hatsTimeFrameModuleAddress?: string, + hatId?: string, + wearer?: string, + count?: number +) => { + const [activeState, setActiveState] = useState({ + isActive: false, + woreTime: 0, + wearingElapsedTime: 0, + }); + + useEffect(() => { + const fetch = async () => { + if (!hatsTimeFrameModuleAddress || !hatId || !wearer) return; + + try { + const [isActive, woreTime, wearingElapsedTime] = await Promise.all([ + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "isActive", + args: [BigInt(hatId), wearer as Address], + }), + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWoreTime", + args: [wearer as Address, BigInt(hatId)], + }), + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWearingElapsedTime", + args: [wearer as Address, BigInt(hatId)], + }), + ]); + + setActiveState({ + isActive, + woreTime: Number(woreTime), + wearingElapsedTime: Number(wearingElapsedTime), + }); + } catch (error) { + console.error(error); + } + }; + + fetch(); + }, [hatsTimeFrameModuleAddress, hatId, wearer, count]); + + return activeState; +}; + +export const useWearingElapsedTime = ( + hatsTimeFrameModuleAddress?: Address, + hatId?: string, + wearers?: string[] +) => { + const [wearingElapsedTimeList, setWearingElapsedTimeList] = useState< + { + wearer: string; + time: number; + }[] + >([]); + + useEffect(() => { + const fetch = async () => { + if (!hatsTimeFrameModuleAddress || !hatId || !wearers) return; + + try { + const list = await Promise.all( + wearers.map(async (w) => { + const time = Number( + await publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWearingElapsedTime", + args: [w as Address, BigInt(hatId)], + }) + ); + + return { wearer: w, time }; + }) + ); + + setWearingElapsedTimeList(list); + } catch (error) { + console.error(error); + } + }; + + fetch(); + }, [hatsTimeFrameModuleAddress, hatId, wearers]); + + return wearingElapsedTimeList; +}; diff --git a/pkgs/frontend/hooks/useWallet.ts b/pkgs/frontend/hooks/useWallet.ts index fa04d90..017b83b 100644 --- a/pkgs/frontend/hooks/useWallet.ts +++ b/pkgs/frontend/hooks/useWallet.ts @@ -1,13 +1,10 @@ +import { ConnectedWallet, usePrivy, useWallets } from "@privy-io/react-auth"; +import { createSmartAccountClient, SmartAccountClient } from "permissionless"; import { - type ConnectedWallet, - usePrivy, - useWallets, -} from "@privy-io/react-auth"; -import { - type SmartAccountClient, - createSmartAccountClient, -} from "permissionless"; -import { toSimpleSmartAccount } from "permissionless/accounts"; + toSimpleSmartAccount, + toThirdwebSmartAccount, +} from "permissionless/accounts"; + import { createPimlicoClient } from "permissionless/clients/pimlico"; import { useCallback, useEffect, useMemo, useState } from "react"; import { @@ -69,7 +66,8 @@ export const useSmartAccountClient = (wallets: ConnectedWallet[]) => { const owner = await embeddedWallet?.getEthereumProvider(); if (!owner) return; - const smartAccount = await toSimpleSmartAccount({ + // We are using thirdweb smart account + const smartAccount = await toThirdwebSmartAccount({ owner, client: publicClient as any, entryPoint: { diff --git a/pkgs/subgraph/README.md b/pkgs/subgraph/README.md index aeec3b7..bf4a7c9 100644 --- a/pkgs/subgraph/README.md +++ b/pkgs/subgraph/README.md @@ -1,6 +1,6 @@ -# POAP Subgraph +# Toban Subgraph -This Subgraph sources events from the POAP contract in different networks. It has been forked from [the original](https://github.com/poap-xyz/poap-subgraph) to demonstrate uses on Goldsky. +This Subgraph sources events from the Toban related contract in different networks. ## Deploying the subgraph: @@ -10,11 +10,7 @@ This Subgraph sources events from the POAP contract in different networks. It ha yarn install ``` -Available networks: mainnet, xdai, chiado, goerli - -**Chiado deployment** - -Chiado is not index by The Graph so we use Goldsky +**deployment** First run: @@ -22,56 +18,13 @@ First run: goldsky login ``` -If you already have an existing Chiado subgraph you will have to delete it to deploy the new one +If you already have an existing subgraph you will have to delete it to deploy the new one **Deploy** -``` ---product hosted-service --access-token {TOKEN} -``` - -as extra parameters just after "graph deploy" in the package json and then execute the following: - ```ssh yarn prepare: yarn codegen yarn build yarn deploy: ``` - -**Good practices** -A good practice to deploy in mainnet or xdai is to have a duplicate/backup subgraph so that if something goes wrong, the traffic can be redirected to the duplicate subgraph instead of having to wait for the subgraph to re-deploy/rollback to a previous version. In Xdai/Gnosis it can take at least 2 days to sync. - -To build a duplicate, you need to create a new subgraph through the-graph profile. Once the new path is provided you can use the next curl to deploy a duplicate WITHOUT NEEDING to resync all over again just by copying the ID of the subgraph you are trying to duplicate. - -```ssh -curl -H "content-type: application/json" -H "authorization: Bearer {TOKEN}" --data '{"jsonrpc": "2.0", "method": "subgraph_deploy", "params": { "name": "poap-xyz/{duplicate_subgraph_path}", "ipfs_hash": "{ID_HASH_FOUND_IN_THE_ORIGINAL_SUBGRAPH}"}, "id": "1"}' https://api.thegraph.com/deploy/ -``` - -## Deployments - -### Mainnet - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap](https://api.thegraph.com/subgraphs/name/poap-xyz/poap) \ -Subgraph page: [https://thegraph.com/explorer/subgraph/poap-xyz/poap](https://thegraph.com/explorer/subgraph/poap-xyz/poap) - -### XDai - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai](https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai) \ -Subgraph page: [https://thegraph.com/explorer/subgraph/poap-xyz/poap-xdai](https://thegraph.com/explorer/subgraph/poap-xyz/poap-xdai) - -### Chiado - -Endpoint: [https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn](https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn) \ -Subgraph page: [https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn](https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn) - -### Goerli - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap-goerli](https://api.thegraph.com/subgraphs/name/poap-xyz/poap-goerli) \ -Subgraph page: [https://thegraph.com/hosted-service/subgraph/poap-xyz/poap-goerli](https://thegraph.com/hosted-service/subgraph/poap-xyz/poap-goerli) - -## Notes - -### Sokol - -Previously none of the params of EventToken was indexed, due to a change in the ABI, newer events now have one of the params indexed and this may cause some issues with the-graph having to deal with malformed or missing entities for older tokens.