From c823e05c79e341819269fd0b88ba18fbdd9b5d50 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:27:29 -0700 Subject: [PATCH 01/63] test deploy on fork --- components/factory/modals/denomInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/factory/modals/denomInfo.tsx b/components/factory/modals/denomInfo.tsx index 3ddf3ea8..11980279 100644 --- a/components/factory/modals/denomInfo.tsx +++ b/components/factory/modals/denomInfo.tsx @@ -2,7 +2,7 @@ import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; import { chainName } from "@/config"; import { useFeeEstimation, useTx } from "@/hooks"; import { cosmos, manifest } from "@chalabi/manifestjs"; -import { Coin } from "@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin"; + import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; import { MsgBurnHeldBalance, From 6d78ad0ee5260587f682942c495f52fc56d848d7 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:15:48 -0700 Subject: [PATCH 02/63] test deploy on fork --- components/react/views/QRCode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/react/views/QRCode.tsx b/components/react/views/QRCode.tsx index e2550f8b..c68da684 100644 --- a/components/react/views/QRCode.tsx +++ b/components/react/views/QRCode.tsx @@ -1,5 +1,5 @@ /* eslint-disable @next/next/no-img-element */ -import { useChain } from "@cosmos-kit/react"; + import { Dialog } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; import { ChevronLeftIcon } from "@heroicons/react/20/solid"; From 1165e3d57dceee9c1de3f82c5f087d5889a161fa Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:16:52 -0700 Subject: [PATCH 03/63] filter bad/done props --- .../groups/components/groupProposals.tsx | 54 ++++++++++++------- components/groups/components/myGroups.tsx | 19 ++++++- components/groups/modals/updateGroupModal.tsx | 12 ++++- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/components/groups/components/groupProposals.tsx b/components/groups/components/groupProposals.tsx index abb98eb7..cba85fcf 100644 --- a/components/groups/components/groupProposals.tsx +++ b/components/groups/components/groupProposals.tsx @@ -326,7 +326,7 @@ export default function ProposalsForPolicy({ )} - {proposals.length > 0 && ( + {filterProposals(proposals).length > 0 && (
@@ -335,7 +335,9 @@ export default function ProposalsForPolicy({ ACTIVE - {proposals.length} + + {filterProposals(proposals).length} +
@@ -346,7 +348,7 @@ export default function ProposalsForPolicy({ { - proposals.filter( + filterProposals(proposals).filter( (proposal) => proposal.executor_result.toString() === "PROPOSAL_EXECUTOR_RESULT_NOT_RUN" && @@ -387,23 +389,37 @@ export default function ProposalsForPolicy({ ENDING - # - {proposals - .reduce((closest, proposal) => { - const proposalDate = new Date( - proposal.voting_period_end - ).getTime(); - const closestDate = new Date( - closest.voting_period_end - ).getTime(); + {(() => { + const activeProposals = filterProposals(proposals); + const now = new Date().getTime(); + const futureActiveProposals = activeProposals.filter( + (proposal) => + new Date(proposal.voting_period_end).getTime() > + now + ); + + if (futureActiveProposals.length === 0) { + return "No active proposals ending soon"; + } + + const closestEndingProposal = + futureActiveProposals.reduce( + (closest, proposal) => { + const proposalDate = new Date( + proposal.voting_period_end + ).getTime(); + const closestDate = new Date( + closest.voting_period_end + ).getTime(); + + return proposalDate - now < closestDate - now + ? proposal + : closest; + } + ); - return Math.abs( - proposalDate - new Date().getTime() - ) < Math.abs(closestDate - new Date().getTime()) - ? proposal - : closest; - }, proposals[0]) - ?.id.toString() || "No proposal ending soon"} + return `#${closestEndingProposal.id.toString()}`; + })()}
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 823d3361..e94a06f4 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -3,6 +3,7 @@ import ProfileAvatar from "@/utils/identicon"; import { truncateString } from "@/utils"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; +import { ProposalSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; export function YourGroups({ groups, @@ -69,6 +70,15 @@ export function YourGroups({ group.ipfsMetadata?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ); + const filterProposals = (proposals: ProposalSDKType[]) => { + return proposals.filter( + (proposal) => + proposal.status !== "PROPOSAL_STATUS_ACCEPTED" && + proposal.status !== "PROPOSAL_STATUS_REJECTED" && + proposal.status !== "PROPOSAL_STATUS_WITHDRAWN" + ); + }; + return (
@@ -99,10 +109,15 @@ export function YourGroups({ }`} onClick={() => handleGroupSelect(policyAddress)} > - {proposals[group?.policies[0]?.address ?? ""]?.length > 0 && ( + {filterProposals(proposals[group?.policies[0]?.address ?? ""]) + .length > 0 && (
- {proposals[group?.policies[0]?.address ?? ""]?.length} + { + filterProposals( + proposals[group?.policies[0]?.address ?? ""] + )?.length + }
)} diff --git a/components/groups/modals/updateGroupModal.tsx b/components/groups/modals/updateGroupModal.tsx index 03c511dc..02968ebe 100644 --- a/components/groups/modals/updateGroupModal.tsx +++ b/components/groups/modals/updateGroupModal.tsx @@ -656,7 +656,17 @@ export function UpdateGroupModal({
- +
); diff --git a/components/factory/components/DenomImage.tsx b/components/factory/components/DenomImage.tsx index 080c957c..cf276420 100644 --- a/components/factory/components/DenomImage.tsx +++ b/components/factory/components/DenomImage.tsx @@ -49,14 +49,22 @@ export const DenomImage = ({ denom }: { denom: any }) => { const [isSupported, setIsSupported] = useState(false); useEffect(() => { - if (denom.uri) { - setIsSupported(isUrlSupported(denom.uri)); - } - setIsLoading(false); + const checkUri = async () => { + if (denom.uri) { + setIsSupported(isUrlSupported(denom.uri)); + // Simulate a delay to show the loading state + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + setIsLoading(false); + }; + + checkUri(); }, [denom.uri]); if (isLoading) { - return
; + return ( +
+ ); } // Check for MFX token first @@ -67,7 +75,7 @@ export const DenomImage = ({ denom }: { denom: any }) => { height={0} src="/logo.svg" alt="MFX Token Icon" - className=" w-[28px] h-[28px] ml-1" + className="w-[28px] h-[28px] ml-1" /> ); } diff --git a/components/factory/components/DenomInfo.tsx b/components/factory/components/DenomInfo.tsx index ad3ea51b..b02f1981 100644 --- a/components/factory/components/DenomInfo.tsx +++ b/components/factory/components/DenomInfo.tsx @@ -22,7 +22,12 @@ export default function DenomInfo({ }) { const DenomConversion = ({ denom }: { denom: MetadataSDKType }) => { if (!denom || !denom.denom_units || denom.denom_units.length === 0) { - return null; + return ( +
+ 1 {0} = {1} +  {1} +
+ ); } const displayUnit = denom.display; diff --git a/components/factory/components/MyDenoms.tsx b/components/factory/components/MyDenoms.tsx index 3d89ed06..2623c889 100644 --- a/components/factory/components/MyDenoms.tsx +++ b/components/factory/components/MyDenoms.tsx @@ -3,6 +3,7 @@ import { useRef, useState, useEffect } from "react"; import { useRouter } from "next/router"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; import { DenomImage } from "./DenomImage"; + export default function MyDenoms({ denoms, isLoading, diff --git a/components/factory/components/metaBox.tsx b/components/factory/components/metaBox.tsx index 3e35051b..21359448 100644 --- a/components/factory/components/metaBox.tsx +++ b/components/factory/components/metaBox.tsx @@ -3,6 +3,7 @@ import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1 import MintForm from "@/components/factory/forms/MintForm"; import BurnForm from "@/components/factory/forms/BurnForm"; import TransferForm from "@/components/factory/forms/TransferForm"; +import { usePoaParams } from "@/hooks"; export default function MetaBox({ denom, @@ -15,9 +16,11 @@ export default function MetaBox({ refetch: () => void; balance: string; }) { + const { poaParams } = usePoaParams(); const [activeTab, setActiveTab] = useState<"transfer" | "burn" | "mint">( "mint" ); + const admin = poaParams?.admins[0]; if (!denom) { return ( @@ -43,7 +46,11 @@ export default function MetaBox({ role="tablist" className="tabs tabs-lifted tabs-md -mr-4 items-end" > - {["transfer", "burn", "mint"].map((tab) => ( + {[ + ...(denom.base.includes("mfx") ? [] : ["transfer"]), + "burn", + "mint", + ].map((tab) => ( + {isMFX && ( + + )} + + {isMFX && ( + setIsModalOpen(false)} + burnPairs={burnPairs} + updateBurnPair={updateBurnPair} + addBurnPair={addBurnPair} + removeBurnPair={removeBurnPair} + handleMultiBurn={handleMultiBurn} + isSigning={isSigning} + /> + )} ); } diff --git a/components/factory/forms/MintForm.tsx b/components/factory/forms/MintForm.tsx index 259345aa..10ddfcbe 100644 --- a/components/factory/forms/MintForm.tsx +++ b/components/factory/forms/MintForm.tsx @@ -1,17 +1,27 @@ import React, { useState } from "react"; import { chainName } from "@/config"; import { useFeeEstimation, useTx } from "@/hooks"; -import { osmosis } from "@chalabi/manifestjs"; +import { cosmos, manifest, osmosis } from "@chalabi/manifestjs"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; -import { PiAddressBook } from "react-icons/pi"; +import { PiAddressBook, PiPlusCircle, PiMinusCircle } from "react-icons/pi"; import { shiftDigits } from "@/utils"; +import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; +import { MsgPayout } from "@chalabi/manifestjs/dist/codegen/manifest/v1/tx"; +import { MultiMintModal } from "../modals/multiMfxMintModal"; + +interface PayoutPair { + address: string; + amount: string; +} export default function MintForm({ + admin, denom, address, refetch, balance, }: { + admin: string; denom: MetadataSDKType; address: string; refetch: () => void; @@ -20,12 +30,22 @@ export default function MintForm({ const [amount, setAmount] = useState(""); const [recipient, setRecipient] = useState(address); const [isSigning, setIsSigning] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [payoutPairs, setPayoutPairs] = useState([ + { address: "", amount: "" }, + ]); + const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); const { mint } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; + const { payout } = manifest.v1.MessageComposer.withTypeUrl; + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; + const exponent = - denom.denom_units.find((unit) => unit.denom === denom.display)?.exponent || - 0; + denom?.denom_units?.find((unit) => unit.denom === denom.display) + ?.exponent || 0; + const isMFX = denom.base.includes("mfx"); + const handleMint = async () => { if (!amount || isNaN(Number(amount))) { return; @@ -35,14 +55,42 @@ export default function MintForm({ const amountInBaseUnits = BigInt( parseFloat(amount) * Math.pow(10, exponent) ).toString(); - const msg = mint({ - amount: { - amount: amountInBaseUnits, - denom: denom.base, - }, - sender: address, - mintToAddress: recipient, - }); + + let msg; + if (isMFX) { + const payoutMsg = payout({ + authority: admin ?? "", + payoutPairs: [ + { + address: recipient, + coin: { denom: denom.base, amount: amountInBaseUnits }, + }, + ], + }); + const encodedMessage = Any.fromPartial({ + typeUrl: payoutMsg.typeUrl, + value: MsgPayout.encode(payoutMsg.value).finish(), + }); + msg = submitProposal({ + groupPolicyAddress: admin ?? "", + messages: [encodedMessage], + metadata: "", + proposers: [address ?? ""], + title: `Manifest Module Control: Mint MFX`, + summary: `This proposal includes a mint action for MFX.`, + exec: 0, + }); + } else { + msg = mint({ + amount: { + amount: amountInBaseUnits, + denom: denom.base, + }, + sender: address, + mintToAddress: recipient, + }); + } + const fee = await estimateFee(address ?? "", [msg]); await tx([msg], { fee, @@ -58,13 +106,75 @@ export default function MintForm({ } }; - const handleAddressBookClick = (e: React.MouseEvent) => { - e.preventDefault(); - setRecipient(address); + const handleMultiMint = async () => { + if ( + payoutPairs.some( + (pair) => !pair.address || !pair.amount || isNaN(Number(pair.amount)) + ) + ) { + alert("Please fill in all fields with valid values."); + return; + } + setIsSigning(true); + try { + const payoutMsg = payout({ + authority: admin ?? "", + payoutPairs: payoutPairs.map((pair) => ({ + address: pair.address, + coin: { + denom: denom.base, + amount: BigInt( + parseFloat(pair.amount) * Math.pow(10, exponent) + ).toString(), + }, + })), + }); + const encodedMessage = Any.fromPartial({ + typeUrl: payoutMsg.typeUrl, + value: MsgPayout.encode(payoutMsg.value).finish(), + }); + const msg = submitProposal({ + groupPolicyAddress: admin ?? "", + messages: [encodedMessage], + metadata: "", + proposers: [address ?? ""], + title: `Manifest Module Control: Multi Mint MFX`, + summary: `This proposal includes multiple mint actions for MFX.`, + exec: 0, + }); + + const fee = await estimateFee(address ?? "", [msg]); + await tx([msg], { + fee, + onSuccess: () => { + setPayoutPairs([{ address: "", amount: "" }]); + setIsModalOpen(false); + refetch(); + }, + }); + } catch (error) { + console.error("Error during multi-minting:", error); + } finally { + setIsSigning(false); + } + }; + + const addPayoutPair = () => + setPayoutPairs([...payoutPairs, { address: "", amount: "" }]); + const removePayoutPair = (index: number) => + setPayoutPairs(payoutPairs.filter((_, i) => i !== index)); + const updatePayoutPair = ( + index: number, + field: "address" | "amount", + value: string + ) => { + const newPairs = [...payoutPairs]; + newPairs[index][field] = value; + setPayoutPairs(newPairs); }; return ( -
+
@@ -80,7 +190,7 @@ export default function MintForm({

EXPONENT

- {denom.denom_units[1].exponent} + {denom?.denom_units[1]?.exponent}

@@ -93,7 +203,7 @@ export default function MintForm({
setRecipient(e.target.value)} /> @@ -125,10 +235,10 @@ export default function MintForm({
-
+
+ {isMFX && ( + + )} + setIsModalOpen(false)} + payoutPairs={payoutPairs} + updatePayoutPair={updatePayoutPair} + addPayoutPair={addPayoutPair} + removePayoutPair={removePayoutPair} + handleMultiMint={handleMultiMint} + isSigning={isSigning} + />
); diff --git a/components/factory/modals/denomInfo.tsx b/components/factory/modals/denomInfo.tsx index 11980279..00738222 100644 --- a/components/factory/modals/denomInfo.tsx +++ b/components/factory/modals/denomInfo.tsx @@ -1,240 +1,14 @@ import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; -import { cosmos, manifest } from "@chalabi/manifestjs"; - -import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; -import { - MsgBurnHeldBalance, - MsgPayout, -} from "@chalabi/manifestjs/dist/codegen/manifest/v1/tx"; -import { useChain } from "@cosmos-kit/react"; -import { useState } from "react"; -import { - FiChevronLeft, - FiChevronRight, - FiPlusCircle, - FiMinusCircle, -} from "react-icons/fi"; type MessageType = "payout" | "burn"; export function DenomInfoModal({ denom, modalId, - isMFX, - admin, - isMember, }: { denom: any; modalId: string; - isMFX?: boolean; - admin?: string; - isMember?: boolean; }) { - const { payout, burnHeldBalance } = manifest.v1.MessageComposer.withTypeUrl; - const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; - const { tx, isSigning, setIsSigning } = useTx(chainName); - const { estimateFee } = useFeeEstimation(chainName); - const { address } = useChain(chainName); - const [activeTab, setActiveTab] = useState("payout"); - const [currentMessageIndex, setCurrentMessageIndex] = useState(0); - const messageTypes: MessageType[] = ["payout", "burn"]; - const [messages, setMessages] = useState<{ - payout: MsgPayout; - burn: MsgBurnHeldBalance; - }>({ - payout: { - authority: admin ?? "", - payoutPairs: [{ address: "", coin: { denom: "", amount: "" } }], - }, - burn: { - authority: admin ?? "", - burnCoins: [{ denom: "", amount: "" }], - }, - }); - - const handleChange = (field: string, value: any) => { - setMessages((prevMessages) => ({ - ...prevMessages, - [activeTab]: { - ...prevMessages[activeTab], - [field]: value, - }, - })); - }; - - const PayoutPairsInputs = () => ( -
- {messages.payout.payoutPairs.map((pair, index) => ( -
-
- - Mint MFX #{index + 1} - - -
-
-
- - - handleChange(`payoutPairs.${index}.address`, e.target.value) - } - /> -
-
- - - handleChange( - `payoutPairs.${index}.coin.denom`, - e.target.value - ) - } - /> -
-
- - - handleChange( - `payoutPairs.${index}.coin.amount`, - e.target.value - ) - } - /> -
-
-
- ))} -
- ); - - const BurnCoinsInputs = () => ( -
- {messages.burn.burnCoins.map((coin, index) => ( -
-
- - Burn MFX #{index + 1} - - -
-
-
- - - handleChange(`burnCoins.${index}.denom`, e.target.value) - } - /> -
-
- - - handleChange(`burnCoins.${index}.amount`, e.target.value) - } - /> -
-
-
- ))} -
- ); - - const handleSubmitProposal = async () => { - setIsSigning(true); - let encodedMessage; - switch (activeTab) { - case "payout": - encodedMessage = Any.fromPartial({ - typeUrl: MsgPayout.typeUrl, - value: MsgPayout.encode(payout(messages.payout).value).finish(), - }); - break; - case "burn": - encodedMessage = Any.fromPartial({ - typeUrl: MsgBurnHeldBalance.typeUrl, - value: MsgBurnHeldBalance.encode( - burnHeldBalance(messages.burn).value - ).finish(), - }); - break; - } - - const msg = submitProposal({ - groupPolicyAddress: address ?? "", - messages: [encodedMessage ?? ({} as Any)], - metadata: "", - proposers: [address ?? ""], - title: `Manifest Module Control: ${activeTab}`, - summary: `This proposal includes a ${activeTab} action.`, - exec: 0, - }); - const fee = await estimateFee(address ?? "", [msg]); - await tx([msg], { - fee, - onSuccess: () => { - setIsSigning(false); - }, - }); - }; - - const toggleManifestControls = () => { - const modal = document.getElementById( - "manifest-controls-modal" - ) as HTMLDialogElement; - if (modal) { - modal.showModal(); - } - }; - return ( <> @@ -279,7 +53,9 @@ export function DenomInfoModal({

EXPONENT

-

{denom.denom_units[1].exponent}

+

+ {denom?.denom_units[1]?.exponent ?? "0"} +

@@ -308,132 +84,40 @@ export function DenomInfoModal({ ))}
- {isMFX === false ? ( -
-

Additional Information

-
-
-
-

BASE

-
- -
+ +
+

Additional Information

+
+
+
+

BASE

+
+
-
-

DISPLAY

-
-

- {denom.display ?? "No display available"} -

-
+
+
+

DISPLAY

+
+

+ {denom.display ?? "No display available"} +

-
-

URI HASH

-
-

- {denom.uri_hash ?? "No URI hash available"} -

-
+
+
+

URI HASH

+
+

+ {denom.uri_hash ?? "No URI hash available"} +

- ) : ( -
- -
- )} +
- <> - {isMFX && ( - -
-
- -
-

Mint & Burn MFX

-
-
-
- {["payout", "burn"].map((tab) => ( - - ))} -
- -
-
- {activeTab === "payout" && } - {activeTab === "burn" && } -
- -
- -
- -
-
-
-
- -
-
- )} - ); } diff --git a/components/factory/modals/multiMfxBurnModal.tsx b/components/factory/modals/multiMfxBurnModal.tsx new file mode 100644 index 00000000..f679bdcd --- /dev/null +++ b/components/factory/modals/multiMfxBurnModal.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { PiPlusCircle, PiMinusCircle } from "react-icons/pi"; + +interface BurnPair { + address: string; + amount: string; +} + +interface MultiBurnModalProps { + isOpen: boolean; + onClose: () => void; + burnPairs: BurnPair[]; + updateBurnPair: ( + index: number, + field: "address" | "amount", + value: string + ) => void; + addBurnPair: () => void; + removeBurnPair: (index: number) => void; + handleMultiBurn: () => void; + isSigning: boolean; +} + +export function MultiBurnModal({ + isOpen, + onClose, + burnPairs, + updateBurnPair, + addBurnPair, + removeBurnPair, + handleMultiBurn, + isSigning, +}: MultiBurnModalProps) { + return ( + +
+

Multi Burn MFX

+
+
+ {burnPairs.map((pair, index) => ( +
+
+ + + updateBurnPair(index, "address", e.target.value) + } + /> +
+
+ + + updateBurnPair(index, "amount", e.target.value) + } + /> +
+ +
+ ))} +
+
+ + +
+
+
+ +
+
+ ); +} diff --git a/components/factory/modals/multiMfxMintModal.tsx b/components/factory/modals/multiMfxMintModal.tsx new file mode 100644 index 00000000..6f4c5e78 --- /dev/null +++ b/components/factory/modals/multiMfxMintModal.tsx @@ -0,0 +1,120 @@ +// MultiMintModal.tsx +import React from "react"; +import { PiPlusCircle, PiMinusCircle } from "react-icons/pi"; + +interface PayoutPair { + address: string; + amount: string; +} + +interface MultiMintModalProps { + isOpen: boolean; + onClose: () => void; + payoutPairs: PayoutPair[]; + updatePayoutPair: ( + index: number, + field: "address" | "amount", + value: string + ) => void; + addPayoutPair: () => void; + removePayoutPair: (index: number) => void; + handleMultiMint: () => void; + isSigning: boolean; +} + +export function MultiMintModal({ + isOpen, + onClose, + payoutPairs, + updatePayoutPair, + addPayoutPair, + removePayoutPair, + handleMultiMint, + isSigning, +}: MultiMintModalProps) { + return ( + +
+

Multi Mint MFX

+
+
+ {payoutPairs.map((pair, index) => ( +
+
+ + + updatePayoutPair(index, "address", e.target.value) + } + /> +
+
+ + + updatePayoutPair(index, "amount", e.target.value) + } + /> +
+ +
+ ))} +
+
+ + +
+
+
+ +
+
+ ); +} diff --git a/components/factory/modals/updateDenomMetadata.tsx b/components/factory/modals/updateDenomMetadata.tsx index 9451e09d..19f2fb64 100644 --- a/components/factory/modals/updateDenomMetadata.tsx +++ b/components/factory/modals/updateDenomMetadata.tsx @@ -27,8 +27,8 @@ export function UpdateDenomMetadataModal({ uri: denom.uri, uriHash: denom.uri_hash, subdenom: denom.base.split("/").pop() || "", - exponent: denom.denom_units[1].exponent.toString(), - label: denom.denom_units[1].denom, + exponent: denom?.denom_units[1]?.exponent?.toString() ?? "6", + label: denom?.denom_units[1]?.denom ?? "mfx", }); const [isSigning, setIsSigning] = useState(false); diff --git a/components/groups/components/groupProposals.tsx b/components/groups/components/groupProposals.tsx index cba85fcf..dae7508f 100644 --- a/components/groups/components/groupProposals.tsx +++ b/components/groups/components/groupProposals.tsx @@ -158,9 +158,9 @@ export default function ProposalsForPolicy({ const filterProposals = (proposals: ProposalSDKType[]) => { return proposals.filter( (proposal) => - proposal.status !== "PROPOSAL_STATUS_ACCEPTED" && - proposal.status !== "PROPOSAL_STATUS_REJECTED" && - proposal.status !== "PROPOSAL_STATUS_WITHDRAWN" + proposal.status.toString() !== "PROPOSAL_STATUS_ACCEPTED" && + proposal.status.toString() !== "PROPOSAL_STATUS_REJECTED" && + proposal.status.toString() !== "PROPOSAL_STATUS_WITHDRAWN" ); }; diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index e94a06f4..41da8e60 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -73,9 +73,9 @@ export function YourGroups({ const filterProposals = (proposals: ProposalSDKType[]) => { return proposals.filter( (proposal) => - proposal.status !== "PROPOSAL_STATUS_ACCEPTED" && - proposal.status !== "PROPOSAL_STATUS_REJECTED" && - proposal.status !== "PROPOSAL_STATUS_WITHDRAWN" + proposal.status.toString() !== "PROPOSAL_STATUS_ACCEPTED" && + proposal.status.toString() !== "PROPOSAL_STATUS_REJECTED" && + proposal.status.toString() !== "PROPOSAL_STATUS_WITHDRAWN" ); }; diff --git a/pages/bank.tsx b/pages/bank.tsx index 997f1025..4f42bf35 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -177,12 +177,8 @@ export default function Bank() { address={address ?? ""} />
diff --git a/pages/factory/index.tsx b/pages/factory/index.tsx index 8ea7cb6c..52d16de6 100644 --- a/pages/factory/index.tsx +++ b/pages/factory/index.tsx @@ -2,6 +2,8 @@ import { WalletSection } from "@/components"; import DenomInfo from "@/components/factory/components/DenomInfo"; import MyDenoms from "@/components/factory/components/MyDenoms"; import { + useBalance, + useTokenBalances, useTokenFactoryBalance, useTokenFactoryDenoms, useTokenFactoryDenomsMetadata, @@ -18,11 +20,26 @@ import MetaBox from "@/components/factory/components/metaBox"; import { CoinSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/base/v1beta1/coin"; export default function Factory() { + const MFX_TOKEN_DATA: MetadataSDKType = { + description: "The native token of the Manifest Chain", + denom_units: [ + { denom: "umfx", exponent: 0, aliases: [] }, + { denom: "mfx", exponent: 6, aliases: [] }, + ], + base: "umfx", + display: "mfx", + name: "Manifest", + symbol: "MFX", + uri: "", + uri_hash: "", + }; + const { address, isWalletConnected } = useChain(chainName); const { denoms, isDenomsLoading, isDenomsError, refetchDenoms } = useTokenFactoryDenoms(address ?? ""); const { metadatas, isMetadatasLoading, isMetadatasError, refetchMetadatas } = useTokenFactoryDenomsMetadata(); + const { balance: mfxBalance } = useBalance(address ?? ""); const [selectedDenom, setSelectedDenom] = useState(null); const [selectedDenomMetadata, setSelectedDenomMetadata] = @@ -54,8 +71,10 @@ export default function Factory() { // Combine denoms and metadatas const combinedData = useMemo(() => { + let result: MetadataSDKType[] = [MFX_TOKEN_DATA]; // Start with MFX data + if (denoms && metadatas) { - return denoms.denoms + const tokenFactoryDenoms = denoms.denoms .map((denom: string) => { return ( metadatas.metadatas.find((meta) => meta.base === denom) || null @@ -64,8 +83,11 @@ export default function Factory() { .filter( (meta: MetadataSDKType | null) => meta !== null ) as MetadataSDKType[]; + + result = [...result, ...tokenFactoryDenoms]; } - return []; + + return result; }, [denoms, metadatas]); const handleDenomSelect = (denom: MetadataSDKType) => { @@ -84,7 +106,6 @@ export default function Factory() { return ( <>
- {/* ... (keep the existing Head content) ... */}

diff --git a/utils/maths.ts b/utils/maths.ts index 564edfa0..76b35a24 100644 --- a/utils/maths.ts +++ b/utils/maths.ts @@ -9,12 +9,30 @@ export const isGreaterThanZero = ( export const shiftDigits = ( num: string | number, places: number, - decimalPlaces?: number, -) => { - return new BigNumber(num) - .shiftedBy(places) - .decimalPlaces(decimalPlaces || 6) - .toString(); + decimalPlaces?: number +): string => { + + if (num === '' || num === null || num === undefined || Number.isNaN(Number(num))) { + console.warn(`Invalid number passed to shiftDigits: ${num}`); + return '0'; + } + + try { + const result = new BigNumber(num) + .shiftedBy(places) + .decimalPlaces(decimalPlaces ?? 6, BigNumber.ROUND_DOWN); + + + if (result.isNaN()) { + console.warn(`Calculation resulted in NaN: ${num}, ${places}`); + return '0'; + } + + return result.toString(); + } catch (error) { + console.error(`Error in shiftDigits: ${error}`); + return '0'; + } }; export const toNumber = ( From 67353c2272c034a867a1de04fee4c47eadff48a3 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:34:48 -0700 Subject: [PATCH 05/63] add tx history to bank --- bun.lockb | Bin 486362 -> 483586 bytes components/admins/modals/validatorModal.tsx | 4 + components/bank/components/historyBox.tsx | 115 ++++++++++++ components/bank/components/index.ts | 14 +- components/bank/components/sendBox.tsx | 6 +- components/bank/components/tokenList.tsx | 6 +- components/bank/index.ts | 3 +- components/bank/modals/index.ts | 1 + components/bank/modals/txInfo.tsx | 130 +++++++++++++ components/factory/components/DenomInfo.tsx | 13 +- components/factory/components/MyDenoms.tsx | 2 +- components/factory/components/metaBox.tsx | 75 +++++--- components/factory/forms/BurnForm.tsx | 12 +- components/factory/forms/MintForm.tsx | 194 +++++++++++--------- components/factory/forms/TransferForm.tsx | 8 +- components/groups/components/myGroups.tsx | 2 +- components/groups/modals/voteModal.tsx | 2 +- hooks/useQueries.ts | 73 +++++++- package.json | 3 +- pages/bank.tsx | 16 +- pages/factory/index.tsx | 19 +- 21 files changed, 546 insertions(+), 152 deletions(-) create mode 100644 components/bank/components/historyBox.tsx create mode 100644 components/bank/modals/index.ts create mode 100644 components/bank/modals/txInfo.tsx diff --git a/bun.lockb b/bun.lockb index f16d29e5ac66573d62db3a1e1de4bb4570419533..e4334e3038a3c470ede9434e455c9e283fb6578a 100755 GIT binary patch delta 103140 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 GlkPv5h?J!O delta 101704 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 +} + +export interface TransactionGroup { + tx_hash: string; + block_number: number; + formatted_date: string; + data: Transaction; +} + +export function HistoryBox({ + isLoading, + send, + address, +}: { + isLoading: boolean; + send: TransactionGroup[]; + address: string; +}) { + const [selectedTx, setSelectedTx] = useState(null); + + function formatDateShort(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + const openModal = (tx: TransactionGroup) => { + setSelectedTx(tx); + }; + + const closeModal = () => { + setSelectedTx(null); + }; + + return ( +
+
+
+
+

Tx History

+
+
+
+ + + + + + + + + + + {send?.map((tx: TransactionGroup) => ( + openModal(tx)} + className="cursor-pointer hover:bg-base-200" + > + + + + + + ))} + +
DateTypeAmountTarget
+ {formatDateShort(tx.formatted_date)} + + {tx.data.from_address === address ? "Send" : "Receive"} + + {tx.data.amount + .map( + (amt) => + `${shiftDigits(amt.amount, -6)} ${formatDenom( + amt.denom + )}` + ) + .join(", ")} + + +
+
+
+
+ {selectedTx && ( + + )} +
+ ); +} diff --git a/components/bank/components/index.ts b/components/bank/components/index.ts index b19149e7..2debfff9 100644 --- a/components/bank/components/index.ts +++ b/components/bank/components/index.ts @@ -1 +1,13 @@ -export * from './sendBox' \ No newline at end of file +export * from './sendBox' +export * from './tokenList' +export * from './historyBox' + +export function formatDenom(denom: string): string { + const cleanDenom = denom.replace(/^factory\/[^/]+\//, ""); + + if (cleanDenom.startsWith("u")) { + return cleanDenom.slice(1).toUpperCase(); + } + + return cleanDenom; + } \ No newline at end of file diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index da32d8c4..06d30f4f 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -28,10 +28,10 @@ export default function SendBox({ ]; return ( -
+
-
-

+
+

{isIbcTransfer ? "IBC Transfer" : "Send Tokens"}

diff --git a/components/bank/components/tokenList.tsx b/components/bank/components/tokenList.tsx index 47c63dba..36f116b5 100644 --- a/components/bank/components/tokenList.tsx +++ b/components/bank/components/tokenList.tsx @@ -32,7 +32,7 @@ export default function TokenList({ balances, isLoading }: TokenListProps) { }; return ( -
+

Your Balances

@@ -50,8 +50,8 @@ export default function TokenList({ balances, isLoading }: TokenListProps) {
{isLoading &&
} {filteredBalances.length > 0 && !isLoading && ( -
- +
+
diff --git a/components/bank/index.ts b/components/bank/index.ts index ca91ae7d..14a8dd39 100644 --- a/components/bank/index.ts +++ b/components/bank/index.ts @@ -1,3 +1,4 @@ export * from "./forms"; -export * from "./components"; \ No newline at end of file +export * from "./components"; +export * from "./modals"; \ No newline at end of file diff --git a/components/bank/modals/index.ts b/components/bank/modals/index.ts new file mode 100644 index 00000000..96a96106 --- /dev/null +++ b/components/bank/modals/index.ts @@ -0,0 +1 @@ +export * from "./txInfo"; \ No newline at end of file diff --git a/components/bank/modals/txInfo.tsx b/components/bank/modals/txInfo.tsx new file mode 100644 index 00000000..4290bf98 --- /dev/null +++ b/components/bank/modals/txInfo.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; +import { formatDenom, TransactionGroup } from "@/components"; +import { FaExternalLinkAlt } from "react-icons/fa"; +import { shiftDigits } from "@/utils"; + +interface TxInfoModalProps { + tx: TransactionGroup; + isOpen: boolean; + onClose: () => void; +} + +export default function TxInfoModal({ tx, isOpen, onClose }: TxInfoModalProps) { + function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } + + return ( + +
+
+ + +

Transaction Details

+
+
+
+
+

TRANSACTION HASH

+
+
+ + + + +
+
+
+
+

BLOCK

+
+

{tx.block_number}

+
+
+
+

TIMESTAMP

+
+

{formatDate(tx.formatted_date)}

+
+
+
+
+
+

FROM

+
+
+ + + + +
+
+
+
+

TO

+
+
+ + + + +
+
+
+
+

VALUE

+
+ {tx.data.amount.map((amt, index) => ( +

+ {shiftDigits(amt.amount, -6)} {formatDenom(amt.denom)} +

+ ))} +
+
+
+
+
+
+ + +
+ ); +} diff --git a/components/factory/components/DenomInfo.tsx b/components/factory/components/DenomInfo.tsx index b02f1981..b458af9d 100644 --- a/components/factory/components/DenomInfo.tsx +++ b/components/factory/components/DenomInfo.tsx @@ -35,7 +35,7 @@ export default function DenomInfo({ const conversionFactor = 10 ** denom.denom_units[1]?.exponent; return ( -
+
1 {displayUnit} = {conversionFactor.toLocaleString()}  {baseUnit?.toUpperCase()}
@@ -48,7 +48,7 @@ export default function DenomInfo({
-
+

Metadata

@@ -71,11 +72,11 @@ export default function DenomInfo({ )} {denom && (
-
+
TICKER - + {denom?.display ?? "No Ticker available"}
@@ -127,7 +128,7 @@ export default function DenomInfo({ SYMBOL -
+
{denom?.symbol ?? "No Description"}
diff --git a/components/factory/components/metaBox.tsx b/components/factory/components/metaBox.tsx index 21359448..df631c8b 100644 --- a/components/factory/components/metaBox.tsx +++ b/components/factory/components/metaBox.tsx @@ -1,26 +1,30 @@ -import { useState } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; import MintForm from "@/components/factory/forms/MintForm"; import BurnForm from "@/components/factory/forms/BurnForm"; import TransferForm from "@/components/factory/forms/TransferForm"; -import { usePoaParams } from "@/hooks"; +import { useGroupsByAdmin, usePoaParams } from "@/hooks"; export default function MetaBox({ denom, address, refetch, balance, + isAdmin, + isLoading, + admin, }: { denom: MetadataSDKType | null; address: string; refetch: () => void; balance: string; + isAdmin: boolean; + isLoading: boolean; + admin: string; }) { - const { poaParams } = usePoaParams(); const [activeTab, setActiveTab] = useState<"transfer" | "burn" | "mint">( "mint" ); - const admin = poaParams?.admins[0]; if (!denom) { return ( @@ -37,14 +41,14 @@ export default function MetaBox({ return (
-

+

{`${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} ${ denom.display }`}

{[ ...(denom.base.includes("mfx") ? [] : ["transfer"]), @@ -70,31 +74,42 @@ export default function MetaBox({ activeTab != "mint" ? "rounded-tr-md" : "" } min-h-[19rem] max-h-[19rem] border-base-300 bg-base-300 `} > - {!denom.base.includes("mfx") && activeTab === "transfer" && ( - + {isLoading && !denom && ( +
+
+
)} - {activeTab === "burn" && ( - - )} - {activeTab === "mint" && ( - + {denom && ( + <> + {!denom.base.includes("mfx") && activeTab === "transfer" && ( + + )} + {activeTab === "burn" && ( + + )} + {activeTab === "mint" && ( + + )} + )}
diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index 89f5343a..58157170 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -15,12 +15,14 @@ interface BurnPair { } export default function BurnForm({ + isAdmin, admin, denom, address, refetch, balance, }: { + isAdmin: boolean; admin: string; denom: MetadataSDKType; address: string; @@ -176,7 +178,9 @@ export default function BurnForm({

NAME

-

{denom.name}

+

+ {denom.name} +

YOUR BALANCE

@@ -191,8 +195,10 @@ export default function BurnForm({

-

CIRCULATING SUPPLY

-

{denom.display}

+

CIRCULATING SUPPLY

+

+ {denom.display} +

diff --git a/components/factory/forms/MintForm.tsx b/components/factory/forms/MintForm.tsx index 10ddfcbe..06d7b7b6 100644 --- a/components/factory/forms/MintForm.tsx +++ b/components/factory/forms/MintForm.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { chainName } from "@/config"; -import { useFeeEstimation, useTx } from "@/hooks"; +import { useFeeEstimation, useGroupsByAdmin, useTx } from "@/hooks"; import { cosmos, manifest, osmosis } from "@chalabi/manifestjs"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; import { PiAddressBook, PiPlusCircle, PiMinusCircle } from "react-icons/pi"; @@ -20,12 +20,14 @@ export default function MintForm({ address, refetch, balance, + isAdmin, }: { admin: string; denom: MetadataSDKType; address: string; refetch: () => void; balance: string; + isAdmin: boolean; }) { const [amount, setAmount] = useState(""); const [recipient, setRecipient] = useState(address); @@ -129,8 +131,8 @@ export default function MintForm({ }, })), }); - const encodedMessage = Any.fromPartial({ - typeUrl: payoutMsg.typeUrl, + const encodedMessage = Any.fromAmino({ + type: payoutMsg.typeUrl, value: MsgPayout.encode(payoutMsg.value).finish(), }); const msg = submitProposal({ @@ -176,95 +178,107 @@ export default function MintForm({ return (
-
-
-

NAME

-

{denom.name}

+ {isMFX && !isAdmin ? ( +
+ You are not affiliated with any PoA Admin entity.
-
-

YOUR BALANCE

-

- {shiftDigits(balance, -exponent)} -

-
-
-

EXPONENT

-

- {denom?.denom_units[1]?.exponent} -

-
-
-

CIRCULATING SUPPLY

-

{denom.display}

-
-
-
- -
-
- - setAmount(e.target.value)} - /> -
-
- -
- setRecipient(e.target.value)} - /> - -
-
-
- -
- - {isMFX && ( - + ) : ( + <> + <> +
+
+

NAME

+

+ {denom.name} +

+
+
+

YOUR BALANCE

+

+ {shiftDigits(balance, -exponent)} +

+
+
+

EXPONENT

+

+ {denom?.denom_units[1]?.exponent} +

+
+
+

CIRCULATING SUPPLY

+

+ {denom.display} +

+
+
+
+
+ + setAmount(e.target.value)} + /> +
+
+ +
+ setRecipient(e.target.value)} + /> + +
+
+
+ +
+ + {isMFX && ( + + )} + setIsModalOpen(false)} + payoutPairs={payoutPairs} + updatePayoutPair={updatePayoutPair} + addPayoutPair={addPayoutPair} + removePayoutPair={removePayoutPair} + handleMultiMint={handleMultiMint} + isSigning={isSigning} + /> +
+ )} - setIsModalOpen(false)} - payoutPairs={payoutPairs} - updatePayoutPair={updatePayoutPair} - addPayoutPair={addPayoutPair} - removePayoutPair={removePayoutPair} - handleMultiMint={handleMultiMint} - isSigning={isSigning} - />
); diff --git a/components/factory/forms/TransferForm.tsx b/components/factory/forms/TransferForm.tsx index 627f06ed..67e4239f 100644 --- a/components/factory/forms/TransferForm.tsx +++ b/components/factory/forms/TransferForm.tsx @@ -32,7 +32,7 @@ export default function TransferForm({ setIsSigning(true); try { const exponent = - denom.denom_units.find((unit) => unit.denom === denom.display) + denom?.denom_units?.find((unit) => unit.denom === denom.display) ?.exponent || 0; const amountInBaseUnits = BigInt( parseFloat(amount) * Math.pow(10, exponent) @@ -82,12 +82,14 @@ export default function TransferForm({

CIRCULATING SUPPLY

-

{denom.symbol}

+

+ {denom.symbol} +

EXPONENT

- {denom.denom_units[1].exponent} + {denom?.denom_units[1]?.exponent}

diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 41da8e60..5dbfa862 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -89,7 +89,7 @@ export function YourGroups({ placeholder="Search..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="input input-bordered input-sm w-1/3 max-w-xs" + className="input input-bordered input-xs w-1/3 max-w-xs" />
diff --git a/components/groups/modals/voteModal.tsx b/components/groups/modals/voteModal.tsx index 05f2cf3d..b800dfe3 100644 --- a/components/groups/modals/voteModal.tsx +++ b/components/groups/modals/voteModal.tsx @@ -1,6 +1,6 @@ import { useFeeEstimation } from "@/hooks"; import { useTx } from "@/hooks/useTx"; -import { cosmos } from "interchain"; +import { cosmos } from "@chalabi/manifestjs"; import { useChain } from "@cosmos-kit/react"; import React from "react"; diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index 1e8cdf0c..4550bdea 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -8,7 +8,7 @@ import { getLogoUrls } from "@/utils"; import { ExtendedValidatorSDKType } from "@/components"; import { useManifestLcdQueryClient } from "./useManifestLcdQueryClient"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; - +import axios from "axios"; export interface IPFSMetadata { title: string; authors: string; @@ -668,4 +668,73 @@ export const useTokenBalancesResolved = (address: string) => { isBalancesError: balancesQuery.isError, refetchBalances: balancesQuery.refetch, }; -} \ No newline at end of file +} + +export const useSendTxQuery = () => { + const fetchTransactions = async () => { + const url = "http://localhost:9000/transactions/send"; + const response = await axios.get(url); + return response.data; + }; + const sendQuery = useQuery({ + queryKey: ["sendTx"], + queryFn: fetchTransactions, + enabled: true, + }); + + return { + sendTxs: sendQuery.data, + isLoading: sendQuery.isLoading, + isError: sendQuery.isError, + error: sendQuery.error, + }; + }; + +export const useIbcTransferTxQuery = () => { + const fetchTransactions = async () => { + const url = "http://localhost:9000/transactions/ibc_transfer"; + const response = await axios.get(url); + return response.data; + }; + const sendQuery = useQuery({ + queryKey: ["transferTx"], + queryFn: fetchTransactions, + enabled: true, + }); + + return { + sendTxs: sendQuery.data, + isLoading: sendQuery.isLoading, + isError: sendQuery.isError, + error: sendQuery.error, + }; + }; + + + export const useSendTxIncludingAddressQuery = (address: string, direction?: 'send' | 'receive') => { + const fetchTransactions = async () => { + let url = `http://localhost:9000/transactions/send/${address}`; + + if (direction) { + url += `/${direction}`; + } + + const response = await axios.get(url); + return response.data; + }; + + const queryKey = ['sendTx', address, direction]; + + const sendQuery = useQuery({ + queryKey, + queryFn: fetchTransactions, + enabled: !!address, + }); + + return { + sendTxs: sendQuery.data, + isLoading: sendQuery.isLoading, + isError: sendQuery.isError, + error: sendQuery.error, + }; + }; \ No newline at end of file diff --git a/package.json b/package.json index 17f90d14..729f1874 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@chain-registry/assets": "^1.42.1", - "@chalabi/manifestjs": "1.2.0", + "@chalabi/manifestjs": "1.2.1", "@cosmjs/cosmwasm-stargate": "0.32.3", "@cosmjs/stargate": "0.32.3", "@cosmos-kit/react": "2.18.0", @@ -50,7 +50,6 @@ "dayjs": "^1.11.10", "framer-motion": "9.0.7", "identicon.js": "^2.3.3", - "interchain": "^1.10.4", "next": "^14.1.0", "postcss": "^8.4.35", "qrcode.react": "^3.1.0", diff --git a/pages/bank.tsx b/pages/bank.tsx index 4f42bf35..0b0b4d20 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -5,6 +5,8 @@ import { chainName } from "@/config"; import { useGroupsByAdmin, usePoaParams, + useSendTxIncludingAddressQuery, + useSendTxQuery, useTokenBalances, useTokenBalancesResolved, useTokenFactoryDenoms, @@ -15,6 +17,7 @@ import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1 import { useChain } from "@cosmos-kit/react"; import Head from "next/head"; import React, { useMemo } from "react"; +import { HistoryBox } from "@/components"; export type CombinedBalanceInfo = { denom: string; @@ -43,10 +46,6 @@ export default function Bank() { ); const group = groupByAdmin?.groups?.[0]; - const isMember = group?.members?.some( - (member) => member?.member?.address === address - ); - const combinedBalances = useMemo(() => { if (!balances || !resolvedBalances || !metadatas) return []; @@ -78,6 +77,8 @@ export default function Bank() { isMetadatasLoading || isPoaParamsLoading; + const { sendTxs } = useSendTxIncludingAddressQuery(address ?? ""); + return ( <>
@@ -181,6 +182,13 @@ export default function Bank() { isLoading={resolvedLoading} />
+
+ +
) )} diff --git a/pages/factory/index.tsx b/pages/factory/index.tsx index 52d16de6..afee4726 100644 --- a/pages/factory/index.tsx +++ b/pages/factory/index.tsx @@ -3,6 +3,8 @@ import DenomInfo from "@/components/factory/components/DenomInfo"; import MyDenoms from "@/components/factory/components/MyDenoms"; import { useBalance, + useGroupsByAdmin, + usePoaParams, useTokenBalances, useTokenFactoryBalance, useTokenFactoryDenoms, @@ -39,7 +41,11 @@ export default function Factory() { useTokenFactoryDenoms(address ?? ""); const { metadatas, isMetadatasLoading, isMetadatasError, refetchMetadatas } = useTokenFactoryDenomsMetadata(); - const { balance: mfxBalance } = useBalance(address ?? ""); + + const { poaParams, isPoaParamsLoading, refetchPoaParams } = usePoaParams(); + const admin = poaParams?.admins[0]; + const { groupByAdmin, isGroupByAdminLoading, refetchGroupByAdmin } = + useGroupsByAdmin(admin ?? ""); const [selectedDenom, setSelectedDenom] = useState(null); const [selectedDenomMetadata, setSelectedDenomMetadata] = @@ -54,6 +60,12 @@ export default function Factory() { isBalanceLoading: isFetchingBalance, } = useTokenFactoryBalance(address ?? "", selectedDenomMetadata?.base ?? ""); + const members = groupByAdmin?.groups?.[0]?.members; + const isAdmin = members?.some( + (member) => member?.member?.address === address + ); + const isLoading = isPoaParamsLoading || isGroupByAdminLoading; + useEffect(() => { if (selectedDenomMetadata) { setIsBalanceLoading(true); @@ -98,6 +110,8 @@ export default function Factory() { const refetch = async () => { refetchDenoms(); refetchMetadatas(); + refetchGroupByAdmin(); + refetchPoaParams(); if (selectedDenomMetadata) { refetchBalance(); } @@ -169,6 +183,9 @@ export default function Factory() {
Date: Tue, 30 Jul 2024 21:33:09 -0700 Subject: [PATCH 06/63] fix admin query issues in factory page --- components/factory/components/metaBox.tsx | 18 ++++++++++++------ hooks/usePoaLcdQueryClient.ts | 1 - hooks/useQueries.ts | 1 + pages/admins.tsx | 2 +- pages/bank.tsx | 4 ++-- pages/factory/index.tsx | 19 ++----------------- 6 files changed, 18 insertions(+), 27 deletions(-) diff --git a/components/factory/components/metaBox.tsx b/components/factory/components/metaBox.tsx index df631c8b..3c204850 100644 --- a/components/factory/components/metaBox.tsx +++ b/components/factory/components/metaBox.tsx @@ -10,22 +10,28 @@ export default function MetaBox({ address, refetch, balance, - isAdmin, - isLoading, - admin, }: { denom: MetadataSDKType | null; address: string; refetch: () => void; balance: string; - isAdmin: boolean; - isLoading: boolean; - admin: string; }) { const [activeTab, setActiveTab] = useState<"transfer" | "burn" | "mint">( "mint" ); + const { poaParams, isPoaParamsLoading, refetchPoaParams, isPoaParamsError } = + usePoaParams(); + const admin = poaParams?.admins[0]; + const { groupByAdmin, isGroupByAdminLoading, refetchGroupByAdmin } = + useGroupsByAdmin(admin ?? ""); + + const members = groupByAdmin?.groups?.[0]?.members; + const isAdmin = members?.some( + (member) => member?.member?.address === address + ); + const isLoading = isPoaParamsLoading || isGroupByAdminLoading; + if (!denom) { return (
diff --git a/hooks/usePoaLcdQueryClient.ts b/hooks/usePoaLcdQueryClient.ts index e13fe716..2214f8d9 100644 --- a/hooks/usePoaLcdQueryClient.ts +++ b/hooks/usePoaLcdQueryClient.ts @@ -9,7 +9,6 @@ import { chainName } from "../config"; const createLcdQueryClient = strangelove_ventures.ClientFactory.createLCDClient; - export const usePoaLcdQueryClient = () => { const { getRestEndpoint } = useChain(chainName); const [resolvedRestEndpoint, setResolvedRestEndpoint] = useState< diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index 4550bdea..df23b71c 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -736,5 +736,6 @@ export const useIbcTransferTxQuery = () => { isLoading: sendQuery.isLoading, isError: sendQuery.isError, error: sendQuery.error, + refetch: sendQuery.refetch, }; }; \ No newline at end of file diff --git a/pages/admins.tsx b/pages/admins.tsx index 93cac062..93ab2702 100644 --- a/pages/admins.tsx +++ b/pages/admins.tsx @@ -31,7 +31,7 @@ export default function Admins() { const { stakingParams, isParamsLoading, refetchParams } = useStakingParams(); const { validators, isActiveValidatorsLoading, refetchActiveValidatorss } = useValidators(); - + console.log(poaParams); const { groupByAdmin, isGroupByAdminLoading, refetchGroupByAdmin } = useGroupsByAdmin( poaParams?.admins[0] ?? diff --git a/pages/bank.tsx b/pages/bank.tsx index 0b0b4d20..e4e0b4c3 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -77,7 +77,7 @@ export default function Bank() { isMetadatasLoading || isPoaParamsLoading; - const { sendTxs } = useSendTxIncludingAddressQuery(address ?? ""); + const { sendTxs, refetch } = useSendTxIncludingAddressQuery(address ?? ""); return ( <> @@ -174,7 +174,7 @@ export default function Bank() { (null); const [selectedDenomMetadata, setSelectedDenomMetadata] = useState(null); @@ -60,12 +55,6 @@ export default function Factory() { isBalanceLoading: isFetchingBalance, } = useTokenFactoryBalance(address ?? "", selectedDenomMetadata?.base ?? ""); - const members = groupByAdmin?.groups?.[0]?.members; - const isAdmin = members?.some( - (member) => member?.member?.address === address - ); - const isLoading = isPoaParamsLoading || isGroupByAdminLoading; - useEffect(() => { if (selectedDenomMetadata) { setIsBalanceLoading(true); @@ -83,7 +72,7 @@ export default function Factory() { // Combine denoms and metadatas const combinedData = useMemo(() => { - let result: MetadataSDKType[] = [MFX_TOKEN_DATA]; // Start with MFX data + let result: MetadataSDKType[] = [MFX_TOKEN_DATA]; if (denoms && metadatas) { const tokenFactoryDenoms = denoms.denoms @@ -110,8 +99,7 @@ export default function Factory() { const refetch = async () => { refetchDenoms(); refetchMetadatas(); - refetchGroupByAdmin(); - refetchPoaParams(); + if (selectedDenomMetadata) { refetchBalance(); } @@ -183,9 +171,6 @@ export default function Factory() {
Date: Tue, 30 Jul 2024 21:42:50 -0700 Subject: [PATCH 07/63] add hardcoded poa address failover --- components/factory/components/metaBox.tsx | 6 +++++- hooks/useQueries.ts | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/components/factory/components/metaBox.tsx b/components/factory/components/metaBox.tsx index 3c204850..cb95abc0 100644 --- a/components/factory/components/metaBox.tsx +++ b/components/factory/components/metaBox.tsx @@ -24,13 +24,17 @@ export default function MetaBox({ usePoaParams(); const admin = poaParams?.admins[0]; const { groupByAdmin, isGroupByAdminLoading, refetchGroupByAdmin } = - useGroupsByAdmin(admin ?? ""); + useGroupsByAdmin( + admin ?? + "manifest1afk9zr2hn2jsac63h4hm60vl9z3e5u69gndzf7c99cqge3vzwjzsfmy9qj" + ); const members = groupByAdmin?.groups?.[0]?.members; const isAdmin = members?.some( (member) => member?.member?.address === address ); const isLoading = isPoaParamsLoading || isGroupByAdminLoading; + console.log({ isAdmin }, { members }, admin); if (!denom) { return ( diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index df23b71c..e93f4dba 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -375,7 +375,7 @@ export const useTokenFactoryBalance = (address: string, denom: string) => { export const usePoaParams = () => { const { lcdQueryClient } = usePoaLcdQueryClient(); - + console.log(lcdQueryClient) const fetchParams = async () => { if (!lcdQueryClient) { throw new Error("LCD Client not ready"); @@ -384,10 +384,11 @@ export const usePoaParams = () => { }; const paramsQuery = useQuery({ - queryKey: ["paramsInfo"], + queryKey: ["paramsInfo", lcdQueryClient], queryFn: fetchParams, enabled: !!lcdQueryClient, staleTime: Infinity, + refetchOnMount: true, }); return { From 945d1b07db3449ea3cb2d68a53dd12b356f99536 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:47:10 -0700 Subject: [PATCH 08/63] add endpoint selection & advanced mode --- bun.lockb | Bin 483586 -> 484386 bytes components/bank/components/historyBox.tsx | 102 +++--- components/factory/components/DenomInfo.tsx | 7 +- components/factory/components/metaBox.tsx | 6 + components/react/endpointSelector.tsx | 360 ++++++++++++++++++++ components/react/index.ts | 1 + components/react/settingsModal.tsx | 43 +++ components/react/sideNav.tsx | 17 +- components/toast.tsx | 122 ++++--- config/defaults.ts | 116 ++++++- contexts/advancedModeContext.tsx | 42 +++ contexts/index.ts | 3 +- hooks/useLocalStorage.tsx | 34 ++ hooks/useQueries.ts | 2 +- package.json | 1 + pages/404.tsx | 19 +- pages/_app.tsx | 44 ++- 17 files changed, 791 insertions(+), 128 deletions(-) create mode 100644 components/react/endpointSelector.tsx create mode 100644 components/react/settingsModal.tsx create mode 100644 contexts/advancedModeContext.tsx create mode 100644 hooks/useLocalStorage.tsx diff --git a/bun.lockb b/bun.lockb index e4334e3038a3c470ede9434e455c9e283fb6578a..4bc3a71c8e34436884bde61297d0f189adb88ce6 100755 GIT binary patch delta 99133 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 am3?SI|H^*$PJ`sfxr4SI?O(aM?f(N&|Az?x delta 98314 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
)!goPmHR%+i5?p#uTYL4YSs_8`D+ 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 diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index 9660be86..b7c3a5a2 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -52,55 +52,61 @@ export function HistoryBox({

-
-
-
Icon
- - - - - - - - - - {send?.map((tx: TransactionGroup) => ( - openModal(tx)} - className="cursor-pointer hover:bg-base-200" - > - - - - +
+ {send?.length > 0 ? ( +
+
DateTypeAmountTarget
- {formatDateShort(tx.formatted_date)} - - {tx.data.from_address === address ? "Send" : "Receive"} - - {tx.data.amount - .map( - (amt) => - `${shiftDigits(amt.amount, -6)} ${formatDenom( - amt.denom - )}` - ) - .join(", ")} - - -
+ + + + + + - ))} - -
DateTypeAmountTarget
-
+ + + {send?.map((tx: TransactionGroup) => ( + openModal(tx)} + className="cursor-pointer hover:bg-base-200" + > + + {formatDateShort(tx.formatted_date)} + + + {tx.data.from_address === address ? "Send" : "Receive"} + + + {tx.data.amount + .map( + (amt) => + `${shiftDigits(amt.amount, -6)} ${formatDenom( + amt.denom + )}` + ) + .join(", ")} + + + + + + ))} + + +
+ ) : ( +
+ No transactions found for this account! +
+ )}
{selectedTx && ( diff --git a/components/factory/components/DenomInfo.tsx b/components/factory/components/DenomInfo.tsx index b458af9d..d047ca1e 100644 --- a/components/factory/components/DenomInfo.tsx +++ b/components/factory/components/DenomInfo.tsx @@ -53,7 +53,7 @@ export default function DenomInfo({ + )} +
+
+ + ))} + + )} +
+ +

+ {isModalOpen && ( +
+
+

Add Custom Endpoint

+
+ + setNewRPCEndpoint(e.target.value)} + /> +
+
+ + setNewAPIEndpoint(e.target.value)} + /> +
+
+ + +
+
+
+ )} +
+ ); +}; diff --git a/components/react/index.ts b/components/react/index.ts index c9a7498d..2e0cabf6 100644 --- a/components/react/index.ts +++ b/components/react/index.ts @@ -1,3 +1,4 @@ export * from './chain-card'; export * from './modal'; export * from './views'; +export * from './settingsModal' diff --git a/components/react/settingsModal.tsx b/components/react/settingsModal.tsx new file mode 100644 index 00000000..76a2cdb6 --- /dev/null +++ b/components/react/settingsModal.tsx @@ -0,0 +1,43 @@ +// components/SettingsModal.tsx + +import React from "react"; +import { useAdvancedMode } from "@/contexts"; + +interface SettingsModalProps { + isOpen: boolean; + onClose: () => void; +} + +const SettingsModal: React.FC = ({ isOpen, onClose }) => { + const { isAdvancedMode, toggleAdvancedMode } = useAdvancedMode(); + + if (!isOpen) return null; + + return ( +
+
+

Settings

+
+
+ +
+
+
+ +
+
+
+ ); +}; + +export default SettingsModal; diff --git a/components/react/sideNav.tsx b/components/react/sideNav.tsx index 9bd2ed3c..8ff6f5e8 100644 --- a/components/react/sideNav.tsx +++ b/components/react/sideNav.tsx @@ -1,4 +1,4 @@ -import { PiSunThin, PiMoonThin } from "react-icons/pi"; +import { PiSunThin, PiMoonThin, PiGearSixThin } from "react-icons/pi"; import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; @@ -11,13 +11,16 @@ import { import { useRouter } from "next/router"; import { IconWallet, WalletSection } from "../wallet"; import { useTheme } from "@/contexts/theme"; +import { useAdvancedMode } from "@/contexts"; +import SettingsModal from "./settingsModal"; export default function SideNav() { const [isDrawerVisible, setDrawerVisible] = useState(false); - + const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [isdark, setIsdark] = useState(false); const { toggleTheme } = useTheme(); + const { isAdvancedMode, toggleAdvancedMode } = useAdvancedMode(); useEffect(() => { const storedIsDark = localStorage.getItem("isdark"); @@ -82,6 +85,12 @@ export default function SideNav() {
+
-
- -
-
- {toastMessage.type === "alert-info" && ( - - )} - {toastMessage.title} -
-
- {toastMessage.description} -
- {toastMessage.link && ( - +
+
+
+ {toastMessage.type === "alert-success" ? ( + + ) : null} + +
+
+ {toastMessage.type === "alert-info" && ( + + )} + {toastMessage.title} +
+
+ {toastMessage.description} +
+ {toastMessage.link && ( + + View on Manifest Scan + + )} +
+
diff --git a/config/defaults.ts b/config/defaults.ts index ef63a920..bba9e729 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -1,11 +1,15 @@ import { AssetList, Chain } from "@chain-registry/types"; export const chainName = process.env.NEXT_PUBLIC_CHAIN ?? 'manifest';; +const mainNetRPC = process.env.NEXT_PUBLIC_MAINNET_RPC ?? 'https://nodes.chandrastation.com/rpc/manifest/'; +const mainNetAPI = process.env.NEXT_PUBLIC_MAINNET_API ?? 'https://nodes.chandrastation.com/api/manifest/'; +const testNetRPC = process.env.NEXT_PUBLIC_TESTNET_RPC ?? 'https://nodes.chandrastation.com/rpc/manifest/'; +const testNetAPI = process.env.NEXT_PUBLIC_TESTNET_API ?? 'https://nodes.chandrastation.com/api/manifest/'; export const manifestChain: Chain = { chain_name: "manifest", status: "live", network_type: "testnet", - website: "https://althea.net/", + website: "", pretty_name: "Manifest Testnet", chain_id: "manifest-1", bech32_prefix: "manifest", @@ -15,14 +19,14 @@ export const manifestChain: Chain = { apis: { rpc: [ { - address: "https://nodes.chandrastation.com/rpc/manifest/", + address: mainNetRPC, provider: undefined, archive: undefined, }, ], rest: [ { - address: "https://nodes.chandrastation.com/api/manifest/", + address: mainNetAPI, provider: undefined, archive: undefined, }, @@ -105,4 +109,110 @@ export const manifestAssets: AssetList = { symbol: "POA", }, ], + }; + + export const manifestTestnetChain: Chain = { + chain_name: "manifest", + status: "live", + network_type: "testnet", + website: "", + pretty_name: "Manifest Testnet", + chain_id: "manifest-1", + bech32_prefix: "manifest", + daemon_name: "manifest", + node_home: "$HOME/.manifest", + slip44: 118, + apis: { + rpc: [ + { + address: testNetRPC, + provider: undefined, + archive: undefined, + }, + ], + rest: [ + { + address: testNetAPI, + provider: undefined, + archive: undefined, + }, + ], + }, + fees: { + fee_tokens: [ + { + denom: "umfx", + fixed_min_gas_price: 0.001, + low_gas_price: 0.001, + average_gas_price: 0.001, + high_gas_price: 0.001, + }, + ], + }, + staking: { + staking_tokens: [ + { + denom: "upoa", + }, + ], + }, + codebase: { + git_repo: "github.com/liftedinit/manifest-ledger", + recommended_version: "v0.0.1-alpha.4", + compatible_versions: ["v0.0.1-alpha.4"], + binaries: { + "linux/amd64": + "https://github.com/liftedinit/manifest-ledger/releases/download/v0.0.1-alpha.4/manifest-ledger_0.0.1-alpha.4_linux_amd64.tar.gz", + }, + versions: [ + { + name: "v1", + recommended_version: "v0.0.1-alpha.4", + compatible_versions: ["v0.0.1-alpha.4"], + }, + ], + genesis: { + genesis_url: + "https://github.com/liftedinit/manifest-ledger/blob/main/network/manifest-1/manifest-1_genesis.json", + }, + }, + }; +export const manifestTestnetAssets: AssetList = { + chain_name: "manifest", + assets: [ + { + description: "Manifest testnet native token", + denom_units: [ + { + denom: "umfx", + exponent: 0, + }, + { + denom: "mfx", + exponent: 6, + }, + ], + base: "umfx", + name: "Manifest Testnet Token", + display: "mfx", + symbol: "MFX", + }, + { + description: "Proof of Authority token for the Manifest testnet", + denom_units: [ + { + denom: "upoa", + exponent: 0, + }, + { + denom: "poa", + exponent: 6, + }, + ], + base: "upoa", + name: "Manifest Testnet Token", + display: "poa", + symbol: "POA", + }, + ], }; \ No newline at end of file diff --git a/contexts/advancedModeContext.tsx b/contexts/advancedModeContext.tsx new file mode 100644 index 00000000..512c68ca --- /dev/null +++ b/contexts/advancedModeContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useState, useContext, ReactNode } from "react"; + +interface AdvancedModeContextType { + isAdvancedMode: boolean; + toggleAdvancedMode: () => void; +} + +const AdvancedModeContext = createContext( + undefined +); + +export const useAdvancedMode = () => { + const context = useContext(AdvancedModeContext); + if (!context) { + throw new Error( + "useAdvancedMode must be used within an AdvancedModeProvider" + ); + } + return context; +}; + +interface AdvancedModeProviderProps { + children: ReactNode; +} + +export const AdvancedModeProvider: React.FC = ({ + children, +}) => { + const [isAdvancedMode, setIsAdvancedMode] = useState(false); + + const toggleAdvancedMode = () => { + setIsAdvancedMode((prev) => !prev); + }; + + return ( + + {children} + + ); +}; diff --git a/contexts/index.ts b/contexts/index.ts index 45c0b519..30ea5bc3 100644 --- a/contexts/index.ts +++ b/contexts/index.ts @@ -1,2 +1,3 @@ export * from "./theme" -export * from "./toastContext" \ No newline at end of file +export * from "./toastContext" +export * from "./advancedModeContext" \ No newline at end of file diff --git a/hooks/useLocalStorage.tsx b/hooks/useLocalStorage.tsx new file mode 100644 index 00000000..975538e6 --- /dev/null +++ b/hooks/useLocalStorage.tsx @@ -0,0 +1,34 @@ +import { useState, useEffect } from "react"; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void] { + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === "undefined") { + return initialValue; + } + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.log(error); + return initialValue; + } + }); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = + value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.log(error); + } + }; + + return [storedValue, setValue]; +} diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index e93f4dba..7d405055 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -375,7 +375,7 @@ export const useTokenFactoryBalance = (address: string, denom: string) => { export const usePoaParams = () => { const { lcdQueryClient } = usePoaLcdQueryClient(); - console.log(lcdQueryClient) + const fetchParams = async () => { if (!lcdQueryClient) { throw new Error("LCD Client not ready"); diff --git a/package.json b/package.json index 729f1874..c7e264fa 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "qrcode.react": "^3.1.0", "react": "18.2.0", "react-apexcharts": "^1.4.1", + "react-confetti": "^6.1.0", "react-dom": "18.2.0", "react-icons": "^5.0.1", "tailwindcss": "^3.4.1" diff --git a/pages/404.tsx b/pages/404.tsx index 2be79f08..3fa0b8f5 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -4,6 +4,7 @@ import { BookOpenIcon, QueueListIcon, RssIcon, + MagnifyingGlassCircleIcon, } from "@heroicons/react/24/solid"; import Head from "next/head"; import Link from "next/link"; @@ -11,21 +12,23 @@ import { SVGProps } from "react"; const links = [ { - name: "Documentation", + name: "Docs", href: "#", - description: "Learn how to integrate our tools with your app.", + description: + "Learn how to sync nodes, query data, and use the Manifest Network.", icon: BookOpenIcon, }, { - name: "API Reference", + name: "Explorer", href: "#", - description: "A complete API reference for our libraries.", - icon: QueueListIcon, + description: "Search for transactions, wallets, and other chain data.", + icon: MagnifyingGlassCircleIcon, }, { - name: "Guides", + name: "FAQ", href: "#", - description: "Installation guides that cover popular setups.", + description: + "The most common questions and answers about the Manifest Network.", icon: BookmarkSquareIcon, }, { @@ -36,7 +39,7 @@ const links = [ }, ]; -export default function FourOFour() { +export default function FourOhFour() { return (
diff --git a/pages/_app.tsx b/pages/_app.tsx index d8fc2329..44dacc38 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -27,9 +27,14 @@ import { cosmosAminoConverters, cosmosProtoRegistry, } from "@chalabi/manifestjs"; -import { ToastProvider } from "@/contexts"; +import { + AdvancedModeProvider, + ToastProvider, + useAdvancedMode, +} from "@/contexts"; import MobileNav from "@/components/react/mobileNav"; +import { EndpointSelector } from "@/components/react/endpointSelector"; // websocket stuff might delete // import * as Ably from "ably"; @@ -171,24 +176,27 @@ function ManifestApp({ Component, pageProps }: AppProps) { > - - -
- -
+ + + +
+ + +
- {/* this is for the web3auth signing modal */} - {isBrowser && - createPortal( - web3AuthPrompt?.resolve(false)} - data={web3AuthPrompt?.signData ?? ({} as SignData)} - approve={() => web3AuthPrompt?.resolve(true)} - reject={() => web3AuthPrompt?.resolve(false)} - />, - document.body - )} + {/* this is for the web3auth signing modal */} + {isBrowser && + createPortal( + web3AuthPrompt?.resolve(false)} + data={web3AuthPrompt?.signData ?? ({} as SignData)} + approve={() => web3AuthPrompt?.resolve(true)} + reject={() => web3AuthPrompt?.resolve(false)} + />, + document.body + )} +
From 2abb27e28f615ca754b8c9bd99fa0185c76c262b Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Sat, 10 Aug 2024 01:36:36 -0700 Subject: [PATCH 09/63] finalize endpoint context provider and component --- components/react/authSignerModal.tsx | 8 +- components/react/endpointSelector.tsx | 131 ++++++++++++++------------ components/react/views/Error.tsx | 8 +- contexts/endpointContext.tsx | 113 ++++++++++++++++++++++ hooks/useLcdQueryClient.ts | 5 +- hooks/useLocalStorage.tsx | 4 +- hooks/useQueries.ts | 31 +++--- pages/_app.tsx | 55 +++++++---- utils/string.ts | 10 +- 9 files changed, 263 insertions(+), 102 deletions(-) create mode 100644 contexts/endpointContext.tsx diff --git a/components/react/authSignerModal.tsx b/components/react/authSignerModal.tsx index d2d1ab16..4004af1b 100644 --- a/components/react/authSignerModal.tsx +++ b/components/react/authSignerModal.tsx @@ -133,16 +133,16 @@ const SignModal = ({ } }, [visible]); + const walletIconString = walletIcon?.toString() ?? ""; + return (
- Wallet type logo

diff --git a/components/react/endpointSelector.tsx b/components/react/endpointSelector.tsx index 984df047..88c7d747 100644 --- a/components/react/endpointSelector.tsx +++ b/components/react/endpointSelector.tsx @@ -1,10 +1,10 @@ -// components/EndpointSelector.tsx - -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAdvancedMode } from "@/contexts"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { useLocalStorage } from "@/hooks/useLocalStorage"; +import { useEndpoint } from "@/contexts/endpointContext"; +import dynamic from "next/dynamic"; interface Endpoint { rpc: string; @@ -12,7 +12,7 @@ interface Endpoint { provider: string; isHealthy: boolean; network: "mainnet" | "testnet"; - custom?: boolean; + custom: boolean; } const predefinedEndpoints: Endpoint[] = [ @@ -22,6 +22,7 @@ const predefinedEndpoints: Endpoint[] = [ provider: "Mainnet", isHealthy: true, network: "mainnet", + custom: false, }, { rpc: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || "", @@ -29,6 +30,7 @@ const predefinedEndpoints: Endpoint[] = [ provider: "Testnet", isHealthy: true, network: "testnet", + custom: false, }, ]; @@ -37,7 +39,8 @@ const validateRPCEndpoint = async (url: string): Promise => { const response = await fetch(`${url}status`); const data = await response.json(); const networkMatches = - data.result.node_info.network === process.env.NEXT_PUBLIC_CHAIN_ID; + data.result.node_info.network === process.env.NEXT_PUBLIC_CHAIN_ID || + "manifest-ledger-beta"; const isNotCatchingUp = !data.result.sync_info.catching_up; return networkMatches && isNotCatchingUp; } catch (error) { @@ -59,7 +62,7 @@ const validateAPIEndpoint = async (url: string): Promise => { } }; -export const EndpointSelector: React.FC = () => { +const EndpointSelector: React.FC = () => { const { isAdvancedMode } = useAdvancedMode(); const [customEndpoints, setCustomEndpoints] = useLocalStorage( "customEndpoints", @@ -67,36 +70,32 @@ export const EndpointSelector: React.FC = () => { ); const [selectedEndpointKey, setSelectedEndpointKey] = useLocalStorage( "selectedEndpoint", - "mainnet" + "Mainnet" ); - const [endpoints, setEndpoints] = useState(predefinedEndpoints); - - useEffect(() => { - const updatedEndpoints = [ - ...predefinedEndpoints, - ...customEndpoints.map((endpoint) => ({ - ...endpoint, - provider: `Custom (${endpoint.network})`, - custom: true, - })), - ]; - setEndpoints(updatedEndpoints); - }, [customEndpoints]); + const { setSelectedEndpoint } = useEndpoint(); + const [localEndpoints, setLocalEndpoints] = useState([]); - const [selectedEndpoint, setSelectedEndpoint] = useState( - predefinedEndpoints[0] - ); + const allEndpoints = useMemo(() => { + return [...predefinedEndpoints, ...customEndpoints]; + }, [predefinedEndpoints, customEndpoints]); useEffect(() => { - const found = endpoints.find((e) => e.provider === selectedEndpointKey); - if (found) { - setSelectedEndpoint(found); - } else if (endpoints.length > 0) { - setSelectedEndpoint(endpoints[0]); - setSelectedEndpointKey(endpoints[0].provider); + const selectedEndpoint = + allEndpoints.find((e) => e.provider === selectedEndpointKey) || + allEndpoints[0]; + + if (selectedEndpoint && selectedEndpoint.provider !== selectedEndpointKey) { + setSelectedEndpoint(selectedEndpoint); + setSelectedEndpointKey(selectedEndpoint.provider); } - }, [endpoints, selectedEndpointKey]); + }, [ + selectedEndpointKey, + allEndpoints, + setSelectedEndpoint, + setSelectedEndpointKey, + ]); + const [isModalOpen, setIsModalOpen] = useState(false); const [newRPCEndpoint, setNewRPCEndpoint] = useState(""); const [newAPIEndpoint, setNewAPIEndpoint] = useState(""); @@ -105,7 +104,7 @@ export const EndpointSelector: React.FC = () => { queryKey: ["checkEndpoints"], queryFn: async () => { const updatedEndpoints = await Promise.all( - endpoints.map(async (endpoint) => ({ + localEndpoints.map(async (endpoint) => ({ ...endpoint, isHealthy: (await validateRPCEndpoint(endpoint.rpc)) && @@ -124,20 +123,11 @@ export const EndpointSelector: React.FC = () => { useEffect(() => { if (data) { - setEndpoints((prevEndpoints) => { - const updatedEndpoints = prevEndpoints.map((endpoint) => { - const matchingEndpoint = data.find( - (e) => e.rpc === endpoint.rpc && e.api === endpoint.api - ); - return matchingEndpoint || endpoint; - }); - return updatedEndpoints; - }); + setLocalEndpoints(data); } }, [data]); const handleEndpointChange = (endpoint: Endpoint) => { - setSelectedEndpoint(endpoint); setSelectedEndpointKey(endpoint.provider); }; @@ -159,7 +149,8 @@ export const EndpointSelector: React.FC = () => { const rpcResponse = await fetch(`${rpcUrl}status`); const rpcData = await rpcResponse.json(); const network = - rpcData.result.node_info.network === process.env.NEXT_PUBLIC_CHAIN_ID + rpcData.result.node_info.network === + process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta" ? "mainnet" : "testnet"; @@ -188,6 +179,23 @@ export const EndpointSelector: React.FC = () => { } }; + const handleRemoveEndpoint = (endpointToRemove: Endpoint) => { + setSelectedEndpoint(predefinedEndpoints[0]); + if (endpointToRemove.custom) { + const updatedCustomEndpoints = customEndpoints.filter( + (endpoint) => endpoint !== endpointToRemove + ); + setCustomEndpoints(updatedCustomEndpoints); + + if (selectedEndpointKey === endpointToRemove.provider) { + const newSelectedEndpoint = + localEndpoints.find((e) => !e.custom) || localEndpoints[0]; + setSelectedEndpoint(newSelectedEndpoint); + setSelectedEndpointKey(newSelectedEndpoint.provider); + } + } + }; + const truncateUrl = (url: string) => { try { const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?$/; @@ -208,18 +216,7 @@ export const EndpointSelector: React.FC = () => { } }; - const handleRemoveEndpoint = (endpointToRemove: Endpoint) => { - setCustomEndpoints((prev: Endpoint[]) => - prev.filter((endpoint) => endpoint !== endpointToRemove) - ); - - if (selectedEndpoint === endpointToRemove) { - const newSelectedEndpoint = - endpoints.find((e) => !e.custom) || endpoints[0]; - setSelectedEndpoint(newSelectedEndpoint); - setSelectedEndpointKey(newSelectedEndpoint.provider); - } - }; + const isCustomEndpoint = (endpoint: Endpoint) => endpoint.custom; return (
{
e.provider === selectedEndpointKey) + ?.isHealthy + ? "bg-primary" + : "bg-secondary" }`} >
- {selectedEndpoint.provider} + {selectedEndpointKey}
@@ -252,11 +252,11 @@ export const EndpointSelector: React.FC = () => {

Error checking endpoints

) : (
    - {endpoints.map((endpoint, index) => ( + {allEndpoints.map((endpoint, index) => (
  • { >
    -

    {endpoint.provider}

    +

    + {endpoint.custom + ? `Custom (${endpoint.network})` + : endpoint.provider} +

    Provider: {truncateUrl(endpoint.rpc)}

    @@ -275,7 +279,7 @@ export const EndpointSelector: React.FC = () => { endpoint.isHealthy ? "bg-primary" : "bg-secondary" }`} >
    - {endpoint.custom && ( + {isCustomEndpoint(endpoint) && (
    ); }; + +export const DynamicEndpointSelector = dynamic( + () => Promise.resolve(EndpointSelector), + { + ssr: false, + } +); diff --git a/components/react/views/Error.tsx b/components/react/views/Error.tsx index 2848d313..16140542 100644 --- a/components/react/views/Error.tsx +++ b/components/react/views/Error.tsx @@ -1,9 +1,9 @@ /* eslint-disable @next/next/no-img-element */ -import { useChain } from "@cosmos-kit/react"; + import { Dialog } from "@headlessui/react"; import { XMarkIcon, ArrowPathIcon } from "@heroicons/react/24/outline"; import { ChevronLeftIcon } from "@heroicons/react/20/solid"; - +import Image from "next/image"; export const Error = ({ currentWalletName, onClose, @@ -45,10 +45,12 @@ export const Error = ({
- Wallet type logo

diff --git a/contexts/endpointContext.tsx b/contexts/endpointContext.tsx new file mode 100644 index 00000000..8c4d6db8 --- /dev/null +++ b/contexts/endpointContext.tsx @@ -0,0 +1,113 @@ +import React, { createContext, useContext, useState, useEffect } from "react"; +import { useLocalStorage } from "@/hooks/useLocalStorage"; + +interface Endpoint { + rpc: string; + api: string; + provider: string; + isHealthy: boolean; + network: "mainnet" | "testnet"; + custom: boolean; +} + +interface EndpointContextType { + selectedEndpoint: Endpoint; + setSelectedEndpoint: (endpoint: Endpoint) => void; + endpoints: Endpoint[]; + setEndpoints: (endpoints: Endpoint[]) => void; +} + +const EndpointContext = createContext( + undefined +); + +export const useEndpoint = () => { + const context = useContext(EndpointContext); + if (!context) { + throw new Error("useEndpoint must be used within an EndpointProvider"); + } + return context; +}; + +interface EndpointProviderProps { + children: React.ReactNode; +} + +export const EndpointProvider: React.FC = ({ + children, +}) => { + const [endpoints, setEndpoints] = useLocalStorage( + "endpoints", + [] + ); + const [selectedEndpointKey, setSelectedEndpointKey] = useLocalStorage( + "selectedEndpointKey", + "Mainnet" + ); + + const [selectedEndpoint, setSelectedEndpoint] = useState({ + rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "", + api: process.env.NEXT_PUBLIC_MAINNET_API_URL || "", + provider: "Mainnet", + isHealthy: true, + network: "mainnet", + custom: false, + }); + + useEffect(() => { + // Initialize endpoints if empty + if (endpoints.length === 0) { + const initialEndpoints: Endpoint[] = [ + { + rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "", + api: process.env.NEXT_PUBLIC_MAINNET_API_URL || "", + provider: "Mainnet", + isHealthy: true, + network: "mainnet", + custom: false, + }, + { + rpc: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || "", + api: process.env.NEXT_PUBLIC_TESTNET_API_URL || "", + provider: "Testnet", + isHealthy: true, + network: "testnet", + custom: false, + }, + ]; + setEndpoints(initialEndpoints); + } + }, [endpoints.length, setEndpoints]); + + useEffect(() => { + const newSelectedEndpoint = endpoints.find( + (e) => e.provider === selectedEndpointKey + ); + + if (newSelectedEndpoint && newSelectedEndpoint !== selectedEndpoint) { + setSelectedEndpoint(newSelectedEndpoint); + } else if ( + endpoints.length > 0 && + selectedEndpoint.provider !== endpoints[0].provider + ) { + setSelectedEndpoint(endpoints[0]); + setSelectedEndpointKey(endpoints[0].provider); + } + }, [endpoints, selectedEndpointKey, setSelectedEndpoint, selectedEndpoint]); + + const contextValue: EndpointContextType = { + selectedEndpoint, + setSelectedEndpoint: (endpoint: Endpoint) => { + setSelectedEndpoint(endpoint); + setSelectedEndpointKey(endpoint.provider); + }, + endpoints, + setEndpoints, + }; + + return ( + + {children} + + ); +}; diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts index 23e59bae..1e7ff9c9 100644 --- a/hooks/useLcdQueryClient.ts +++ b/hooks/useLcdQueryClient.ts @@ -3,6 +3,7 @@ import { cosmos } from "@chalabi/manifestjs"; import { useQuery } from "@tanstack/react-query"; import { useChain } from "@cosmos-kit/react"; import { chainName } from "../config"; +import { useEndpoint } from "@/contexts/endpointContext"; const createLcdQueryClient = cosmos.ClientFactory.createLCDClient; @@ -15,7 +16,7 @@ export const useLcdQueryClient = () => { useEffect(() => { const resolveEndpoint = async () => { const endpoint = await getRestEndpoint(); - + if (typeof endpoint === "string") { setResolvedRestEndpoint(endpoint); } else if (endpoint && typeof endpoint === "object") { @@ -26,6 +27,8 @@ export const useLcdQueryClient = () => { resolveEndpoint(); }, [getRestEndpoint]); + const {selectedEndpoint} = useEndpoint(); + console.log(selectedEndpoint.rpc) const lcdQueryClient = useQuery({ queryKey: ["lcdQueryClient", resolvedRestEndpoint], queryFn: () => diff --git a/hooks/useLocalStorage.tsx b/hooks/useLocalStorage.tsx index 975538e6..13ff1e83 100644 --- a/hooks/useLocalStorage.tsx +++ b/hooks/useLocalStorage.tsx @@ -12,7 +12,7 @@ export function useLocalStorage( const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { - console.log(error); + console.error(error); return initialValue; } }); @@ -26,7 +26,7 @@ export function useLocalStorage( window.localStorage.setItem(key, JSON.stringify(valueToStore)); } } catch (error) { - console.log(error); + console.error(error); } }; diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index 7d405055..a5027503 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -4,7 +4,7 @@ import { QueryGroupsByMemberResponseSDKType } from "@chalabi/manifestjs/dist/cod import { useLcdQueryClient } from "./useLcdQueryClient"; import { usePoaLcdQueryClient } from "./usePoaLcdQueryClient"; -import { getLogoUrls } from "@/utils"; +import { getLogoUrls, isValidIPFSCID } from "@/utils"; import { ExtendedValidatorSDKType } from "@/components"; import { useManifestLcdQueryClient } from "./useManifestLcdQueryClient"; import { MetadataSDKType } from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; @@ -49,19 +49,24 @@ export const useGroupsByMember = (address: string) => { lcdQueryClient?.cosmos.group.v1.groupPoliciesByGroup({ groupId: group.id })); const memberPromises = groupQuery.data.groups.map(group => lcdQueryClient?.cosmos.group.v1.groupMembers({ groupId: group.id })); - const ipfsPromises = groupQuery.data.groups.map(group => - fetch(`https://nodes.chandrastation.com/ipfs/gateway/${group.metadata}`) - .then(response => { - if (!response.ok) { + const ipfsPromises = groupQuery.data.groups.map(group => { + if (isValidIPFSCID(group.metadata)) { + return fetch(`https://nodes.chandrastation.com/ipfs/gateway/${group.metadata}`) + .then(response => { + if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.json() as Promise; - }) - .catch(err => { - console.error("Failed to fetch IPFS data:", err); - return null; // Return null in case of an error - }) - ); + } + return response.json() as Promise; + }) + .catch(err => { + console.error(`Invalid IPFS CID for group #${group?.id}`, err); + return null; + }); + } else { + console.warn(`Invalid IPFS CID for group #${group?.id}`); + return Promise.resolve(null); + } + }); const [policiesResults, membersResults, ipfsResults] = await Promise.all([ Promise.all(policyPromises), diff --git a/pages/_app.tsx b/pages/_app.tsx index 44dacc38..d4e274a6 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -27,14 +27,12 @@ import { cosmosAminoConverters, cosmosProtoRegistry, } from "@chalabi/manifestjs"; -import { - AdvancedModeProvider, - ToastProvider, - useAdvancedMode, -} from "@/contexts"; +import { AdvancedModeProvider, ToastProvider } from "@/contexts"; import MobileNav from "@/components/react/mobileNav"; -import { EndpointSelector } from "@/components/react/endpointSelector"; +import { DynamicEndpointSelector } from "@/components/react/endpointSelector"; +import { EndpointProvider, useEndpoint } from "@/contexts/endpointContext"; +import { useRouter } from "next/router"; // websocket stuff might delete // import * as Ably from "ably"; @@ -44,7 +42,21 @@ import { EndpointSelector } from "@/components/react/endpointSelector"; // key: process.env.NEXT_PUBLIC_ABLY_API_KEY, // }); -function ManifestApp({ Component, pageProps }: AppProps) { +type ManifestAppProps = AppProps & { + Component: AppProps["Component"]; + pageProps: AppProps["pageProps"]; +}; + +function ManifestAppWrapper(props: AppProps) { + return ( + + + + ); +} + +function ManifestApp({ Component, pageProps }: ManifestAppProps) { + const { selectedEndpoint } = useEndpoint(); // signer options to support amino signing for all the different modules we use const signerOptions: SignerOptions = { signingStargate: ( @@ -139,8 +151,22 @@ function ManifestApp({ Component, pageProps }: AppProps) { setIsBrowser(true); }, []); + const endpointOptions = useMemo( + () => ({ + isLazy: true, + endpoints: { + manifest: { + rpc: [selectedEndpoint.rpc], + rest: [selectedEndpoint.api], + }, + }, + }), + [selectedEndpoint] + ); + return ( // +

- +
@@ -201,8 +219,9 @@ function ManifestApp({ Component, pageProps }: AppProps) { + // ); } -export default ManifestApp; +export default ManifestAppWrapper; diff --git a/utils/string.ts b/utils/string.ts index f56360a6..68c6ed5e 100644 --- a/utils/string.ts +++ b/utils/string.ts @@ -11,4 +11,12 @@ } else { return str; } - } \ No newline at end of file + } + + export const isValidIPFSCID = (cid: string): boolean => { + + const cidV0Regex = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/; + const cidV1Regex = /^b[A-Za-z2-7]{58}$/; + return cidV0Regex.test(cid) || cidV1Regex.test(cid); + }; + \ No newline at end of file From cba280f8e9e4aacc8933a8d58a5e14e39d6f852f Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Mon, 19 Aug 2024 01:25:12 -0700 Subject: [PATCH 10/63] use zustand for endpoint state --- components/react/endpointSelector.tsx | 285 ++++++++------------------ components/react/sideNav.tsx | 23 ++- components/toast.tsx | 2 +- contexts/advancedModeContext.tsx | 22 +- contexts/endpointContext.tsx | 113 ---------- hooks/useLcdQueryClient.ts | 11 +- hooks/useLocalStorage.tsx | 34 --- pages/_app.tsx | 27 ++- store/endpointStore.ts | 159 ++++++++++++++ 9 files changed, 300 insertions(+), 376 deletions(-) delete mode 100644 contexts/endpointContext.tsx delete mode 100644 hooks/useLocalStorage.tsx create mode 100644 store/endpointStore.ts diff --git a/components/react/endpointSelector.tsx b/components/react/endpointSelector.tsx index 88c7d747..8f40783d 100644 --- a/components/react/endpointSelector.tsx +++ b/components/react/endpointSelector.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAdvancedMode } from "@/contexts"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { useLocalStorage } from "@/hooks/useLocalStorage"; -import { useEndpoint } from "@/contexts/endpointContext"; + import dynamic from "next/dynamic"; +import { useEndpointStore } from "@/store/endpointStore"; interface Endpoint { rpc: string; @@ -15,123 +15,49 @@ interface Endpoint { custom: boolean; } -const predefinedEndpoints: Endpoint[] = [ - { - rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "", - api: process.env.NEXT_PUBLIC_MAINNET_API_URL || "", - provider: "Mainnet", - isHealthy: true, - network: "mainnet", - custom: false, - }, - { - rpc: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || "", - api: process.env.NEXT_PUBLIC_TESTNET_API_URL || "", - provider: "Testnet", - isHealthy: true, - network: "testnet", - custom: false, - }, -]; - -const validateRPCEndpoint = async (url: string): Promise => { - try { - const response = await fetch(`${url}status`); - const data = await response.json(); - const networkMatches = - data.result.node_info.network === process.env.NEXT_PUBLIC_CHAIN_ID || - "manifest-ledger-beta"; - const isNotCatchingUp = !data.result.sync_info.catching_up; - return networkMatches && isNotCatchingUp; - } catch (error) { - console.error("Error validating RPC endpoint:", error); - return false; - } -}; - -const validateAPIEndpoint = async (url: string): Promise => { - try { - const response = await fetch( - `${url}cosmos/base/tendermint/v1beta1/syncing` - ); - const data = await response.json(); - return !data.syncing; - } catch (error) { - console.error("Error validating API endpoint:", error); - return false; - } -}; - const EndpointSelector: React.FC = () => { const { isAdvancedMode } = useAdvancedMode(); - const [customEndpoints, setCustomEndpoints] = useLocalStorage( - "customEndpoints", - [] - ); - const [selectedEndpointKey, setSelectedEndpointKey] = useLocalStorage( - "selectedEndpoint", - "Mainnet" - ); - - const { setSelectedEndpoint } = useEndpoint(); - const [localEndpoints, setLocalEndpoints] = useState([]); - - const allEndpoints = useMemo(() => { - return [...predefinedEndpoints, ...customEndpoints]; - }, [predefinedEndpoints, customEndpoints]); - - useEffect(() => { - const selectedEndpoint = - allEndpoints.find((e) => e.provider === selectedEndpointKey) || - allEndpoints[0]; - - if (selectedEndpoint && selectedEndpoint.provider !== selectedEndpointKey) { - setSelectedEndpoint(selectedEndpoint); - setSelectedEndpointKey(selectedEndpoint.provider); - } - }, [ + const { + endpoints, selectedEndpointKey, - allEndpoints, - setSelectedEndpoint, + selectedEndpoint, setSelectedEndpointKey, - ]); + addEndpoint, + removeEndpoint, + updateEndpointHealth, + } = useEndpointStore((state) => ({ + endpoints: state.endpoints, + selectedEndpointKey: state.selectedEndpointKey, + selectedEndpoint: state.selectedEndpoint, + setSelectedEndpointKey: state.setSelectedEndpointKey, + addEndpoint: state.addEndpoint, + removeEndpoint: state.removeEndpoint, + updateEndpointHealth: state.updateEndpointHealth, + })); + + const handleEndpointChange = useCallback( + (endpoint: Endpoint) => { + setSelectedEndpointKey(endpoint.provider); + }, + [setSelectedEndpointKey] + ); const [isModalOpen, setIsModalOpen] = useState(false); const [newRPCEndpoint, setNewRPCEndpoint] = useState(""); const [newAPIEndpoint, setNewAPIEndpoint] = useState(""); + const [endpointToRemove, setEndpointToRemove] = useState(null); - const { isLoading, error, data, refetch } = useQuery({ - queryKey: ["checkEndpoints"], - queryFn: async () => { - const updatedEndpoints = await Promise.all( - localEndpoints.map(async (endpoint) => ({ - ...endpoint, - isHealthy: - (await validateRPCEndpoint(endpoint.rpc)) && - (await validateAPIEndpoint(endpoint.api)), - })) - ); - return updatedEndpoints; - }, + const { isLoading, error, refetch, data } = useQuery({ + queryKey: ["checkEndpoints", endpoints], + queryFn: updateEndpointHealth, refetchInterval: 30000, - enabled: false, + enabled: true, }); - useEffect(() => { - refetch(); - }, [refetch]); - - useEffect(() => { - if (data) { - setLocalEndpoints(data); - } - }, [data]); - - const handleEndpointChange = (endpoint: Endpoint) => { - setSelectedEndpointKey(endpoint.provider); - }; - - const handleCustomEndpointSubmit = async () => { + const handleCustomEndpointSubmit = async (e: { + stopPropagation: () => void; + }) => { + e.stopPropagation(); if (!newRPCEndpoint || !newAPIEndpoint) return; const rpcUrl = newRPCEndpoint.startsWith("http") @@ -141,73 +67,26 @@ const EndpointSelector: React.FC = () => { ? newAPIEndpoint : `https://${newAPIEndpoint}`; - const isRPCValid = await validateRPCEndpoint(rpcUrl); - const isAPIValid = await validateAPIEndpoint(apiUrl); - - if (isRPCValid && isAPIValid) { - try { - const rpcResponse = await fetch(`${rpcUrl}status`); - const rpcData = await rpcResponse.json(); - const network = - rpcData.result.node_info.network === - process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta" - ? "mainnet" - : "testnet"; - - const newEndpoint: Endpoint = { - rpc: rpcUrl, - api: apiUrl, - provider: `Custom (${network})`, - isHealthy: true, - network, - custom: true, - }; - - setCustomEndpoints((prev: Endpoint[]) => [...prev, newEndpoint]); - setSelectedEndpointKey(newEndpoint.provider); - setNewRPCEndpoint(""); - setNewAPIEndpoint(""); - setIsModalOpen(false); - } catch (error) { - console.error("Error adding custom endpoint:", error); - alert( - "An error occurred while adding the custom endpoint. Please try again." - ); - } - } else { - alert("Invalid endpoint(s). Please check the URLs and try again."); - } - }; - - const handleRemoveEndpoint = (endpointToRemove: Endpoint) => { - setSelectedEndpoint(predefinedEndpoints[0]); - if (endpointToRemove.custom) { - const updatedCustomEndpoints = customEndpoints.filter( - (endpoint) => endpoint !== endpointToRemove + try { + await addEndpoint(rpcUrl, apiUrl); + setNewRPCEndpoint(""); + setNewAPIEndpoint(""); + setIsModalOpen(false); + } catch (error) { + console.error("Error adding custom endpoint:", error); + alert( + error instanceof Error + ? error.message + : "An error occurred while adding the custom endpoint. Please try again." ); - setCustomEndpoints(updatedCustomEndpoints); - - if (selectedEndpointKey === endpointToRemove.provider) { - const newSelectedEndpoint = - localEndpoints.find((e) => !e.custom) || localEndpoints[0]; - setSelectedEndpoint(newSelectedEndpoint); - setSelectedEndpointKey(newSelectedEndpoint.provider); - } } }; - const truncateUrl = (url: string) => { try { const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?$/; - if (!url.startsWith("http://") && !url.startsWith("https://")) { - if (ipRegex.test(url)) { - url = "http://" + url; - } else { - url = "https://" + url; - } + url = ipRegex.test(url) ? `http://${url}` : `https://${url}`; } - const parsedUrl = new URL(url); return `${parsedUrl.host}/...`; } catch (error) { @@ -231,10 +110,7 @@ const EndpointSelector: React.FC = () => {
e.provider === selectedEndpointKey) - ?.isHealthy - ? "bg-primary" - : "bg-secondary" + selectedEndpoint?.isHealthy ? "bg-primary" : "bg-secondary" }`} >
{selectedEndpointKey} @@ -252,7 +128,7 @@ const EndpointSelector: React.FC = () => {

Error checking endpoints

) : (
    - {allEndpoints.map((endpoint, index) => ( + {endpoints.map((endpoint: Endpoint, index: number) => (
  • { ? "bg-base-200" : "hover:bg-base-200" }`} - onClick={() => handleEndpointChange(endpoint)} + onClick={() => setSelectedEndpointKey(endpoint.provider)} >
    @@ -279,31 +155,49 @@ const EndpointSelector: React.FC = () => { endpoint.isHealthy ? "bg-primary" : "bg-secondary" }`} >
    - {isCustomEndpoint(endpoint) && ( -
    + {isCustomEndpoint(endpoint) && ( + - )} -
+ )} + + + )}
))} @@ -312,7 +206,10 @@ const EndpointSelector: React.FC = () => {
@@ -365,7 +262,5 @@ const EndpointSelector: React.FC = () => { export const DynamicEndpointSelector = dynamic( () => Promise.resolve(EndpointSelector), - { - ssr: false, - } + { ssr: false } ); diff --git a/components/react/sideNav.tsx b/components/react/sideNav.tsx index 8ff6f5e8..38dffe24 100644 --- a/components/react/sideNav.tsx +++ b/components/react/sideNav.tsx @@ -35,26 +35,27 @@ export default function SideNav() { }, [isdark]); const toggleDrawer = () => setDrawerVisible(!isDrawerVisible); + const NavItem: React.FC<{ Icon: React.ElementType; href: string }> = ({ Icon, href, }) => { const { pathname } = useRouter(); - const isActive = pathname === href; - - const iconClassName = `w-8 h-8 transition-all duration-300 ease-in-out ${ - isActive ? "text-primary scale-105" : "hover:text-primary" - }`; + const tooltipText = href.split("/")[1] || href; return ( -
  • +
  • - - -
    - {href.split("/")} -
    +
    + + + {tooltipText} +
  • diff --git a/components/toast.tsx b/components/toast.tsx index 62a52473..b2979695 100644 --- a/components/toast.tsx +++ b/components/toast.tsx @@ -58,7 +58,7 @@ export const Toast: React.FC = ({ if (!toastMessage) { return null; } - console.log(toastMessage.type); + const getGradientColor = (type: string) => { switch (type) { case "alert-success": diff --git a/contexts/advancedModeContext.tsx b/contexts/advancedModeContext.tsx index 512c68ca..e71769e6 100644 --- a/contexts/advancedModeContext.tsx +++ b/contexts/advancedModeContext.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useState, useContext, ReactNode } from "react"; +import React, { + createContext, + useState, + useContext, + ReactNode, + useEffect, +} from "react"; interface AdvancedModeContextType { isAdvancedMode: boolean; @@ -26,10 +32,20 @@ interface AdvancedModeProviderProps { export const AdvancedModeProvider: React.FC = ({ children, }) => { - const [isAdvancedMode, setIsAdvancedMode] = useState(false); + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("isAdvancedMode"); + return saved !== null ? JSON.parse(saved) : false; + } + return false; + }); + + useEffect(() => { + localStorage.setItem("isAdvancedMode", JSON.stringify(isAdvancedMode)); + }, [isAdvancedMode]); const toggleAdvancedMode = () => { - setIsAdvancedMode((prev) => !prev); + setIsAdvancedMode((prev: any) => !prev); }; return ( diff --git a/contexts/endpointContext.tsx b/contexts/endpointContext.tsx deleted file mode 100644 index 8c4d6db8..00000000 --- a/contexts/endpointContext.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; -import { useLocalStorage } from "@/hooks/useLocalStorage"; - -interface Endpoint { - rpc: string; - api: string; - provider: string; - isHealthy: boolean; - network: "mainnet" | "testnet"; - custom: boolean; -} - -interface EndpointContextType { - selectedEndpoint: Endpoint; - setSelectedEndpoint: (endpoint: Endpoint) => void; - endpoints: Endpoint[]; - setEndpoints: (endpoints: Endpoint[]) => void; -} - -const EndpointContext = createContext( - undefined -); - -export const useEndpoint = () => { - const context = useContext(EndpointContext); - if (!context) { - throw new Error("useEndpoint must be used within an EndpointProvider"); - } - return context; -}; - -interface EndpointProviderProps { - children: React.ReactNode; -} - -export const EndpointProvider: React.FC = ({ - children, -}) => { - const [endpoints, setEndpoints] = useLocalStorage( - "endpoints", - [] - ); - const [selectedEndpointKey, setSelectedEndpointKey] = useLocalStorage( - "selectedEndpointKey", - "Mainnet" - ); - - const [selectedEndpoint, setSelectedEndpoint] = useState({ - rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "", - api: process.env.NEXT_PUBLIC_MAINNET_API_URL || "", - provider: "Mainnet", - isHealthy: true, - network: "mainnet", - custom: false, - }); - - useEffect(() => { - // Initialize endpoints if empty - if (endpoints.length === 0) { - const initialEndpoints: Endpoint[] = [ - { - rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "", - api: process.env.NEXT_PUBLIC_MAINNET_API_URL || "", - provider: "Mainnet", - isHealthy: true, - network: "mainnet", - custom: false, - }, - { - rpc: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || "", - api: process.env.NEXT_PUBLIC_TESTNET_API_URL || "", - provider: "Testnet", - isHealthy: true, - network: "testnet", - custom: false, - }, - ]; - setEndpoints(initialEndpoints); - } - }, [endpoints.length, setEndpoints]); - - useEffect(() => { - const newSelectedEndpoint = endpoints.find( - (e) => e.provider === selectedEndpointKey - ); - - if (newSelectedEndpoint && newSelectedEndpoint !== selectedEndpoint) { - setSelectedEndpoint(newSelectedEndpoint); - } else if ( - endpoints.length > 0 && - selectedEndpoint.provider !== endpoints[0].provider - ) { - setSelectedEndpoint(endpoints[0]); - setSelectedEndpointKey(endpoints[0].provider); - } - }, [endpoints, selectedEndpointKey, setSelectedEndpoint, selectedEndpoint]); - - const contextValue: EndpointContextType = { - selectedEndpoint, - setSelectedEndpoint: (endpoint: Endpoint) => { - setSelectedEndpoint(endpoint); - setSelectedEndpointKey(endpoint.provider); - }, - endpoints, - setEndpoints, - }; - - return ( - - {children} - - ); -}; diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts index 1e7ff9c9..41b6459c 100644 --- a/hooks/useLcdQueryClient.ts +++ b/hooks/useLcdQueryClient.ts @@ -3,7 +3,8 @@ import { cosmos } from "@chalabi/manifestjs"; import { useQuery } from "@tanstack/react-query"; import { useChain } from "@cosmos-kit/react"; import { chainName } from "../config"; -import { useEndpoint } from "@/contexts/endpointContext"; +import { useEndpointStore } from "@/store/endpointStore"; + const createLcdQueryClient = cosmos.ClientFactory.createLCDClient; @@ -27,13 +28,13 @@ export const useLcdQueryClient = () => { resolveEndpoint(); }, [getRestEndpoint]); - const {selectedEndpoint} = useEndpoint(); - console.log(selectedEndpoint.rpc) + const {selectedEndpoint} = useEndpointStore(); + console.log(selectedEndpoint) const lcdQueryClient = useQuery({ - queryKey: ["lcdQueryClient", resolvedRestEndpoint], + queryKey: ["lcdQueryClient", selectedEndpoint?.api], queryFn: () => createLcdQueryClient({ - restEndpoint: resolvedRestEndpoint || "", + restEndpoint: selectedEndpoint?.api || "", }), enabled: !!resolvedRestEndpoint, staleTime: Infinity, diff --git a/hooks/useLocalStorage.tsx b/hooks/useLocalStorage.tsx deleted file mode 100644 index 13ff1e83..00000000 --- a/hooks/useLocalStorage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from "react"; - -export function useLocalStorage( - key: string, - initialValue: T -): [T, (value: T | ((val: T) => T)) => void] { - const [storedValue, setStoredValue] = useState(() => { - if (typeof window === "undefined") { - return initialValue; - } - try { - const item = window.localStorage.getItem(key); - return item ? JSON.parse(item) : initialValue; - } catch (error) { - console.error(error); - return initialValue; - } - }); - - const setValue = (value: T | ((val: T) => T)) => { - try { - const valueToStore = - value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - if (typeof window !== "undefined") { - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } - } catch (error) { - console.error(error); - } - }; - - return [storedValue, setValue]; -} diff --git a/pages/_app.tsx b/pages/_app.tsx index d4e274a6..1a707b94 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -31,8 +31,8 @@ import { AdvancedModeProvider, ToastProvider } from "@/contexts"; import MobileNav from "@/components/react/mobileNav"; import { DynamicEndpointSelector } from "@/components/react/endpointSelector"; -import { EndpointProvider, useEndpoint } from "@/contexts/endpointContext"; -import { useRouter } from "next/router"; + +import { useEndpointStore } from "@/store/endpointStore"; // websocket stuff might delete // import * as Ably from "ably"; @@ -47,16 +47,7 @@ type ManifestAppProps = AppProps & { pageProps: AppProps["pageProps"]; }; -function ManifestAppWrapper(props: AppProps) { - return ( - - - - ); -} - function ManifestApp({ Component, pageProps }: ManifestAppProps) { - const { selectedEndpoint } = useEndpoint(); // signer options to support amino signing for all the different modules we use const signerOptions: SignerOptions = { signingStargate: ( @@ -80,6 +71,8 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { }, }; + const { selectedEndpoint } = useEndpointStore(); + // tanstack query client const client = new QueryClient(); @@ -156,8 +149,14 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { isLazy: true, endpoints: { manifest: { - rpc: [selectedEndpoint.rpc], - rest: [selectedEndpoint.api], + rpc: [ + selectedEndpoint?.rpc ?? + "https://nodes.chandrastation.com/rpc/manifest/", + ], + rest: [ + selectedEndpoint?.api ?? + "https://nodes.chandrastation.com/api/manifest/", + ], }, }, }), @@ -224,4 +223,4 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { ); } -export default ManifestAppWrapper; +export default ManifestApp; diff --git a/store/endpointStore.ts b/store/endpointStore.ts new file mode 100644 index 00000000..78096612 --- /dev/null +++ b/store/endpointStore.ts @@ -0,0 +1,159 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface Endpoint { + rpc: string; + api: string; + provider: string; + isHealthy: boolean; + network: 'mainnet' | 'testnet'; + custom: boolean; +} + +interface EndpointState { + endpoints: Endpoint[]; + selectedEndpointKey: string; + selectedEndpoint: Endpoint | null; + setEndpoints: (endpoints: Endpoint[]) => void; + setSelectedEndpointKey: (key: string) => void; + addEndpoint: (rpc: string, api: string) => Promise; + removeEndpoint: (provider: string) => void; + updateEndpointHealth: () => Promise; +} + +const validateRPCEndpoint = async (rpc: string): Promise => { + try { + const url = new URL('status', rpc.trim()); + const response = await fetch(url.toString()); + console.log('RPC response status:', response.status); + const data = await response.json(); + console.log('RPC response data:', data); + + + if (data.result && data.result.node_info && data.result.sync_info) { + const networkMatches = data.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta"); + const isNotCatchingUp = !data.result.sync_info.catching_up; + console.log('Network matches:', networkMatches, 'Not catching up:', isNotCatchingUp); + return true; + } else { + console.log('Unexpected RPC response structure'); + return false; + } + } catch (error) { + console.error("Error validating RPC endpoint:", error); + return false; + } +}; + +const validateAPIEndpoint = async (api: string): Promise => { + try { + const url = new URL('cosmos/base/tendermint/v1beta1/syncing', api.trim()); + const response = await fetch(url.toString()); + return response.ok; + } catch (error) { + console.error('Error validating API endpoint:', error); + return false; + } +}; + +const defaultEndpoints: Endpoint[] = [ + { + rpc: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || '', + api: process.env.NEXT_PUBLIC_MAINNET_API_URL || '', + provider: 'Mainnet', + isHealthy: true, + network: 'mainnet', + custom: false, + }, + { + rpc: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || '', + api: process.env.NEXT_PUBLIC_TESTNET_API_URL || '', + provider: 'Testnet', + isHealthy: true, + network: 'testnet', + custom: false, + }, +]; + +export const useEndpointStore = create( + persist( + (set, get) => ({ + endpoints: defaultEndpoints, + selectedEndpointKey: 'Mainnet', + selectedEndpoint: defaultEndpoints[0], + setEndpoints: (endpoints) => set({ endpoints }), + setSelectedEndpointKey: (key) => { + const endpoint = get().endpoints.find((e) => e.provider === key); + set({ selectedEndpointKey: key, selectedEndpoint: endpoint || null }); + + if (typeof window !== 'undefined') { + window.location.reload(); + } + }, + addEndpoint: async (rpc: string, api: string) => { + try { + const isRPCValid = await validateRPCEndpoint(rpc); + const isAPIValid = await validateAPIEndpoint(api); + + console.log('RPC validation:', isRPCValid, 'API validation:', isAPIValid); + + if (isRPCValid && isAPIValid) { + const rpcResponse = await fetch(`${rpc.trim()}status`); + const rpcData = await rpcResponse.json(); + console.log('RPC data:', rpcData); + + const network = rpcData.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta") ? "mainnet" : "testnet"; + + const newEndpoint: Endpoint = { + rpc: rpc.trim(), + api: api.trim(), + provider: `Custom (${network})`, + isHealthy: true, + network, + custom: true, + }; + + const { endpoints } = get(); + set({ + endpoints: [...endpoints, newEndpoint], + }); + } else { + throw new Error("Invalid endpoint(s). Please check the URLs and try again."); + } + } catch (error) { + console.error('Error in addEndpoint:', error); + throw error; + } + }, + removeEndpoint: (provider) => { + const { endpoints, selectedEndpointKey } = get(); + const newEndpoints = endpoints.filter(e => e.provider !== provider); + set({ endpoints: newEndpoints }); + if (selectedEndpointKey === provider) { + const newSelectedEndpoint = newEndpoints[0]; + set({ + selectedEndpointKey: newSelectedEndpoint.provider, + selectedEndpoint: newSelectedEndpoint + }); + } + }, + updateEndpointHealth: async () => { + const { endpoints } = get(); + const updatedEndpoints = await Promise.all( + endpoints.map(async (endpoint) => ({ + ...endpoint, + isHealthy: + (await validateRPCEndpoint(endpoint.rpc)) && + (await validateAPIEndpoint(endpoint.api)), + })) + ); + set({ endpoints: updatedEndpoints }); + return updatedEndpoints; // Return the updated endpoints + }, + }), + { + name: 'endpoint-storage', + getStorage: () => localStorage, + } + ) +); \ No newline at end of file From 6674421800ce3f720df08f8afeec3fe114c868b4 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:14:35 -0700 Subject: [PATCH 11/63] fix loading state for endpoint swap --- components/factory/components/metaBox.tsx | 1 - components/factory/forms/BurnForm.tsx | 195 +++++++++++----------- components/factory/modals/denomInfo.tsx | 15 +- components/react/endpointSelector.tsx | 2 +- pages/_app.tsx | 124 ++++++++------ store/endpointStore.ts | 4 - 6 files changed, 177 insertions(+), 164 deletions(-) diff --git a/components/factory/components/metaBox.tsx b/components/factory/components/metaBox.tsx index c883ef19..f9d7ad43 100644 --- a/components/factory/components/metaBox.tsx +++ b/components/factory/components/metaBox.tsx @@ -34,7 +34,6 @@ export default function MetaBox({ (member) => member?.member?.address === address ); const isLoading = isPoaParamsLoading || isGroupByAdminLoading; - console.log({ isAdmin }, { members }, admin); useEffect(() => { if (denom?.base.includes("mfx")) { diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index 58157170..3f49ba69 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -173,105 +173,110 @@ export default function BurnForm({ }; return ( -
    +
    -
    -
    -

    NAME

    -

    - {denom.name} -

    + {isMFX && !isAdmin ? ( +
    + You are not affiliated with any PoA Admin entity.
    -
    -

    YOUR BALANCE

    -

    - {shiftDigits(balance, -exponent)} -

    -
    -
    -

    EXPONENT

    -

    - {denom?.denom_units[1]?.exponent} -

    -
    -
    -

    CIRCULATING SUPPLY

    -

    - {denom.display} -

    -
    -
    -
    - -
    -
    - - setAmount(e.target.value)} - /> -
    -
    - -
    - setRecipient(e.target.value)} - /> - -
    -
    -
    - -
    - - {isMFX && ( - + ) : ( + <> +
    +
    +

    NAME

    +

    + {denom.name} +

    +
    +
    +

    YOUR BALANCE

    +

    + {shiftDigits(balance, -exponent)} +

    +
    +
    +

    EXPONENT

    +

    + {denom?.denom_units[1]?.exponent} +

    +
    +
    +

    CIRCULATING SUPPLY

    +

    + {denom.display} +

    +
    +
    +
    +
    + + setAmount(e.target.value)} + /> +
    +
    + +
    + setRecipient(e.target.value)} + /> + +
    +
    +
    +
    + + {isMFX && ( + + )} +
    + {isMFX && ( + setIsModalOpen(false)} + burnPairs={burnPairs} + updateBurnPair={updateBurnPair} + addBurnPair={addBurnPair} + removeBurnPair={removeBurnPair} + handleMultiBurn={handleMultiBurn} + isSigning={isSigning} + /> + )} + )}
    - - {isMFX && ( - setIsModalOpen(false)} - burnPairs={burnPairs} - updateBurnPair={updateBurnPair} - addBurnPair={addBurnPair} - removeBurnPair={removeBurnPair} - handleMultiBurn={handleMultiBurn} - isSigning={isSigning} - /> - )}
    ); } diff --git a/components/factory/modals/denomInfo.tsx b/components/factory/modals/denomInfo.tsx index 00738222..2afe5ff2 100644 --- a/components/factory/modals/denomInfo.tsx +++ b/components/factory/modals/denomInfo.tsx @@ -44,12 +44,7 @@ export function DenomInfoModal({

    -
    -

    URI

    -
    -

    {denom.uri ?? "No URI available"}

    -
    -
    +

    EXPONENT

    @@ -103,14 +98,6 @@ export function DenomInfoModal({

    -
    -

    URI HASH

    -
    -

    - {denom.uri_hash ?? "No URI hash available"} -

    -
    -

    diff --git a/components/react/endpointSelector.tsx b/components/react/endpointSelector.tsx index 8f40783d..3eefe9e9 100644 --- a/components/react/endpointSelector.tsx +++ b/components/react/endpointSelector.tsx @@ -6,7 +6,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline"; import dynamic from "next/dynamic"; import { useEndpointStore } from "@/store/endpointStore"; -interface Endpoint { +export interface Endpoint { rpc: string; api: string; provider: string; diff --git a/pages/_app.tsx b/pages/_app.tsx index 1a707b94..c2d53269 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,7 +6,7 @@ import type { AppProps } from "next/app"; import { createPortal } from "react-dom"; import { SignData } from "@cosmos-kit/web3auth"; import { makeWeb3AuthWallets } from "@cosmos-kit/web3auth/esm/index"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import SignModal from "@/components/react/authSignerModal"; import { manifestAssets, manifestChain } from "@/config"; import { SignerOptions, wallets } from "cosmos-kit"; @@ -72,6 +72,24 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { }; const { selectedEndpoint } = useEndpointStore(); + const [isLoading, setIsLoading] = useState(false); + const [endpointKey, setEndpointKey] = useState(0); + const previousEndpointRef = useRef(); + + useEffect(() => { + if ( + previousEndpointRef.current && + previousEndpointRef.current !== selectedEndpoint + ) { + setIsLoading(true); + const timer = setTimeout(() => { + setEndpointKey((prev) => prev + 1); + setIsLoading(false); + }, 1000); + return () => clearTimeout(timer); + } + previousEndpointRef.current = selectedEndpoint; + }, [selectedEndpoint]); // tanstack query client const client = new QueryClient(); @@ -168,55 +186,63 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { - +
    +

    Swapping endpoints...

    +
    + ) : ( + - - - - - -
    - - -
    - - {/* this is for the web3auth signing modal */} - {isBrowser && - createPortal( - web3AuthPrompt?.resolve(false)} - data={web3AuthPrompt?.signData ?? ({} as SignData)} - approve={() => web3AuthPrompt?.resolve(true)} - reject={() => web3AuthPrompt?.resolve(false)} - />, - document.body - )} -
    -
    -
    -
    + }} + signerOptions={signerOptions} + // @ts-ignore + walletModal={TailwindModal} + > + + + + + +
    + + +
    + + {/* this is for the web3auth signing modal */} + {isBrowser && + createPortal( + web3AuthPrompt?.resolve(false)} + data={web3AuthPrompt?.signData ?? ({} as SignData)} + approve={() => web3AuthPrompt?.resolve(true)} + reject={() => web3AuthPrompt?.resolve(false)} + />, + document.body + )} +
    +
    +
    + + )} // diff --git a/store/endpointStore.ts b/store/endpointStore.ts index 78096612..a4ceb541 100644 --- a/store/endpointStore.ts +++ b/store/endpointStore.ts @@ -85,10 +85,6 @@ export const useEndpointStore = create( setSelectedEndpointKey: (key) => { const endpoint = get().endpoints.find((e) => e.provider === key); set({ selectedEndpointKey: key, selectedEndpoint: endpoint || null }); - - if (typeof window !== 'undefined') { - window.location.reload(); - } }, addEndpoint: async (rpc: string, api: string) => { try { From c4c344696603f9a141edeef2fe27253947039445 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:01:26 -0700 Subject: [PATCH 12/63] use toast for all alerts and errors --- components/factory/forms/BurnForm.tsx | 10 ++++-- components/factory/forms/MintForm.tsx | 10 ++++-- components/react/endpointSelector.tsx | 49 ++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index 3f49ba69..1d643517 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -8,6 +8,7 @@ import { shiftDigits } from "@/utils"; import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; import { MsgBurnHeldBalance } from "@chalabi/manifestjs/dist/codegen/manifest/v1/tx"; import { MultiBurnModal } from "../modals/multiMfxBurnModal"; +import { useToast } from "@/contexts"; interface BurnPair { address: string; @@ -42,7 +43,7 @@ export default function BurnForm({ const { burn } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; const { burnHeldBalance } = manifest.v1.MessageComposer.withTypeUrl; const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; - + const { setToastMessage } = useToast(); const exponent = denom?.denom_units?.find((unit) => unit.denom === denom.display) ?.exponent || 0; @@ -109,7 +110,12 @@ export default function BurnForm({ (pair) => !pair.address || !pair.amount || isNaN(Number(pair.amount)) ) ) { - alert("Please fill in all fields with valid values."); + setToastMessage({ + type: "alert-error", + title: "Missing fields", + description: "Please fill in all fields with valid values.", + bgColor: "#e74c3c", + }); return; } setIsSigning(true); diff --git a/components/factory/forms/MintForm.tsx b/components/factory/forms/MintForm.tsx index 06d7b7b6..fec4b2d9 100644 --- a/components/factory/forms/MintForm.tsx +++ b/components/factory/forms/MintForm.tsx @@ -8,6 +8,7 @@ import { shiftDigits } from "@/utils"; import { Any } from "@chalabi/manifestjs/dist/codegen/google/protobuf/any"; import { MsgPayout } from "@chalabi/manifestjs/dist/codegen/manifest/v1/tx"; import { MultiMintModal } from "../modals/multiMfxMintModal"; +import { useToast } from "@/contexts"; interface PayoutPair { address: string; @@ -36,7 +37,7 @@ export default function MintForm({ const [payoutPairs, setPayoutPairs] = useState([ { address: "", amount: "" }, ]); - + const { setToastMessage } = useToast(); const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); const { mint } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl; @@ -114,7 +115,12 @@ export default function MintForm({ (pair) => !pair.address || !pair.amount || isNaN(Number(pair.amount)) ) ) { - alert("Please fill in all fields with valid values."); + setToastMessage({ + type: "alert-error", + title: "Missing fields", + description: "Please fill in all fields with valid values.", + bgColor: "#e74c3c", + }); return; } setIsSigning(true); diff --git a/components/react/endpointSelector.tsx b/components/react/endpointSelector.tsx index 3eefe9e9..c1fc2912 100644 --- a/components/react/endpointSelector.tsx +++ b/components/react/endpointSelector.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAdvancedMode } from "@/contexts"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; - +import { useToast } from "@/contexts"; import dynamic from "next/dynamic"; import { useEndpointStore } from "@/store/endpointStore"; @@ -34,7 +34,7 @@ const EndpointSelector: React.FC = () => { removeEndpoint: state.removeEndpoint, updateEndpointHealth: state.updateEndpointHealth, })); - + const { setToastMessage } = useToast(); const handleEndpointChange = useCallback( (endpoint: Endpoint) => { setSelectedEndpointKey(endpoint.provider); @@ -58,7 +58,15 @@ const EndpointSelector: React.FC = () => { stopPropagation: () => void; }) => { e.stopPropagation(); - if (!newRPCEndpoint || !newAPIEndpoint) return; + if (!newRPCEndpoint || !newAPIEndpoint) { + setToastMessage({ + type: "alert-error", + title: "Error adding custom endpoint", + description: "Both RPC and API endpoints are required.", + bgColor: "#e74c3c", + }); + return; + } const rpcUrl = newRPCEndpoint.startsWith("http") ? newRPCEndpoint @@ -72,15 +80,40 @@ const EndpointSelector: React.FC = () => { setNewRPCEndpoint(""); setNewAPIEndpoint(""); setIsModalOpen(false); + setToastMessage({ + type: "alert-success", + title: "Custom endpoint added", + description: "The new endpoint has been successfully added.", + bgColor: "#2ecc71", + }); } catch (error) { console.error("Error adding custom endpoint:", error); - alert( - error instanceof Error - ? error.message - : "An error occurred while adding the custom endpoint. Please try again." - ); + let errorMessage = "An unknown error occurred while adding the endpoint."; + + if (error instanceof Error) { + if (error.message.includes("Invalid URL")) { + errorMessage = + "Invalid URL format. Please check both RPC and API URLs."; + } else if (error.message.includes("Network error")) { + errorMessage = + "Network error. Please check your internet connection and try again."; + } else if (error.message.includes("Timeout")) { + errorMessage = + "Connection timeout. The endpoint might be unreachable."; + } else { + errorMessage = error.message; + } + } + + setToastMessage({ + type: "alert-error", + title: "Error adding custom endpoint", + description: errorMessage, + bgColor: "#e74c3c", + }); } }; + const truncateUrl = (url: string) => { try { const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?$/; From db06a19f49c63d0dc546c3553618e176b736958b Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:01:14 -0700 Subject: [PATCH 13/63] update config to work with testnet & main-net chain id's --- config/defaults.ts | 8 ++++---- pages/_app.tsx | 19 ++++++++++++++++--- pages/bank.tsx | 4 +++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/config/defaults.ts b/config/defaults.ts index bba9e729..7d0c1acd 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -117,7 +117,7 @@ export const manifestAssets: AssetList = { network_type: "testnet", website: "", pretty_name: "Manifest Testnet", - chain_id: "manifest-1", + chain_id: "manifest-ledger-beta", bech32_prefix: "manifest", daemon_name: "manifest", node_home: "$HOME/.manifest", @@ -143,9 +143,9 @@ export const manifestAssets: AssetList = { { denom: "umfx", fixed_min_gas_price: 0.001, - low_gas_price: 0.001, - average_gas_price: 0.001, - high_gas_price: 0.001, + low_gas_price: 0.004, + average_gas_price: 0.008, + high_gas_price: 0.01, }, ], }, diff --git a/pages/_app.tsx b/pages/_app.tsx index c2d53269..befdda76 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -8,7 +8,12 @@ import { SignData } from "@cosmos-kit/web3auth"; import { makeWeb3AuthWallets } from "@cosmos-kit/web3auth/esm/index"; import { useEffect, useMemo, useRef, useState } from "react"; import SignModal from "@/components/react/authSignerModal"; -import { manifestAssets, manifestChain } from "@/config"; +import { + manifestAssets, + manifestChain, + manifestTestnetChain, + manifestTestnetAssets, +} from "@/config"; import { SignerOptions, wallets } from "cosmos-kit"; import { ChainProvider } from "@cosmos-kit/react"; import { Registry } from "@cosmjs/proto-signing"; @@ -194,8 +199,16 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { ) : ( Date: Tue, 20 Aug 2024 18:14:05 -0700 Subject: [PATCH 14/63] fix endpoint selector refresh errors --- config/defaults.ts | 8 ++++---- pages/_app.tsx | 4 +++- store/endpointStore.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config/defaults.ts b/config/defaults.ts index 7d0c1acd..8af44ee2 100644 --- a/config/defaults.ts +++ b/config/defaults.ts @@ -2,8 +2,8 @@ import { AssetList, Chain } from "@chain-registry/types"; export const chainName = process.env.NEXT_PUBLIC_CHAIN ?? 'manifest';; const mainNetRPC = process.env.NEXT_PUBLIC_MAINNET_RPC ?? 'https://nodes.chandrastation.com/rpc/manifest/'; const mainNetAPI = process.env.NEXT_PUBLIC_MAINNET_API ?? 'https://nodes.chandrastation.com/api/manifest/'; -const testNetRPC = process.env.NEXT_PUBLIC_TESTNET_RPC ?? 'https://nodes.chandrastation.com/rpc/manifest/'; -const testNetAPI = process.env.NEXT_PUBLIC_TESTNET_API ?? 'https://nodes.chandrastation.com/api/manifest/'; +const testNetRPC = process.env.NEXT_PUBLIC_TESTNET_RPC ?? 'https://manifest-beta-rpc.liftedinit.tech/ '; +const testNetAPI = process.env.NEXT_PUBLIC_TESTNET_API ?? 'https://manifest-beta-rest.liftedinit.tech/ '; export const manifestChain: Chain = { chain_name: "manifest", @@ -11,7 +11,7 @@ export const manifestChain: Chain = { network_type: "testnet", website: "", pretty_name: "Manifest Testnet", - chain_id: "manifest-1", + chain_id: process.env.NEXT_PUBLIC_CHAIN_ID ?? "manifest-1", bech32_prefix: "manifest", daemon_name: "manifest", node_home: "$HOME/.manifest", @@ -117,7 +117,7 @@ export const manifestAssets: AssetList = { network_type: "testnet", website: "", pretty_name: "Manifest Testnet", - chain_id: "manifest-ledger-beta", + chain_id: process.env.NEXT_PUBLIC_TESTNET_CHAIN_ID ?? "manifest-ledger-beta", bech32_prefix: "manifest", daemon_name: "manifest", node_home: "$HOME/.manifest", diff --git a/pages/_app.tsx b/pages/_app.tsx index befdda76..f82131cc 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -79,7 +79,9 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { const { selectedEndpoint } = useEndpointStore(); const [isLoading, setIsLoading] = useState(false); const [endpointKey, setEndpointKey] = useState(0); - const previousEndpointRef = useRef(); + const previousEndpointRef = useRef( + undefined + ); useEffect(() => { if ( diff --git a/store/endpointStore.ts b/store/endpointStore.ts index a4ceb541..78e5acef 100644 --- a/store/endpointStore.ts +++ b/store/endpointStore.ts @@ -31,7 +31,7 @@ const validateRPCEndpoint = async (rpc: string): Promise => { if (data.result && data.result.node_info && data.result.sync_info) { - const networkMatches = data.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta"); + const networkMatches = data.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || process.env.NEXT_PUBLIC_TESTNET_CHAIN_ID); const isNotCatchingUp = !data.result.sync_info.catching_up; console.log('Network matches:', networkMatches, 'Not catching up:', isNotCatchingUp); return true; @@ -98,7 +98,7 @@ export const useEndpointStore = create( const rpcData = await rpcResponse.json(); console.log('RPC data:', rpcData); - const network = rpcData.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || "manifest-ledger-beta") ? "mainnet" : "testnet"; + const network = rpcData.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || process.env.NEXT_PUBLIC_TESTNET_CHAIN_ID) ? "mainnet" : "testnet"; const newEndpoint: Endpoint = { rpc: rpc.trim(), From b4fb39a14209818a7ae076ff135e90e66e070ae7 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:42:11 -0400 Subject: [PATCH 15/63] feat: bun test setup --- bun.lockb | Bin 484386 -> 572066 bytes bunfig.toml | 2 ++ happydom.ts | 3 +++ package.json | 5 +++++ 4 files changed, 10 insertions(+) create mode 100644 bunfig.toml create mode 100644 happydom.ts diff --git a/bun.lockb b/bun.lockb index 4bc3a71c8e34436884bde61297d0f189adb88ce6..71eec92f3df6088147be31d06f54eb9578552af2 100755 GIT binary patch delta 137053 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

    I&x;Y$6z^4-BX6i(swv&~F31>WLK(w}8ty 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 delta 82044 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 diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..d20eff30 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = "./happydom.ts" diff --git a/happydom.ts b/happydom.ts new file mode 100644 index 00000000..9cae201a --- /dev/null +++ b/happydom.ts @@ -0,0 +1,3 @@ +import { GlobalRegistrator } from "@happy-dom/global-registrator"; + +GlobalRegistrator.register(); \ No newline at end of file diff --git a/package.json b/package.json index c7e264fa..5a45ab9f 100644 --- a/package.json +++ b/package.json @@ -61,18 +61,23 @@ "tailwindcss": "^3.4.1" }, "devDependencies": { + "@happy-dom/global-registrator": "^15.0.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.8", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^16.0.0", "@types/crypto-js": "^4.2.2", "@types/identicon.js": "^2.3.4", + "@types/jest": "^29.5.12", "@types/node": "18.11.9", "@types/react": "18.2.55", "@types/react-dom": "18.2.19", "autoprefixer": "^10.4.13", "eslint": "8.56.0", "eslint-config-next": "13.0.5", + "jest": "^29.7.0", "tailwind-scrollbar-hide": "^1.1.7", "typescript": "4.9.3" }, From 620d590472ee2521dd892981aaf7a8c05adfc049 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:42:24 -0400 Subject: [PATCH 16/63] test: token list component --- .../components/__tests__/tokenList.test.tsx | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 components/bank/components/__tests__/tokenList.test.tsx diff --git a/components/bank/components/__tests__/tokenList.test.tsx b/components/bank/components/__tests__/tokenList.test.tsx new file mode 100644 index 00000000..3202ecb8 --- /dev/null +++ b/components/bank/components/__tests__/tokenList.test.tsx @@ -0,0 +1,78 @@ +import { test, expect, afterEach, describe } from "bun:test"; +import React from "react"; +import matchers from '@testing-library/jest-dom/matchers'; +import {fireEvent, render, screen, cleanup} from "@testing-library/react"; +import TokenList from "@/components/bank/components/tokenList"; +import { CombinedBalanceInfo } from "@/pages/bank"; + +expect.extend(matchers); + +const mockBalances: CombinedBalanceInfo[] = [ + { + denom: "token1", + coreDenom: "utoken1", + amount: "1000", + metadata: { + description: "My First Token", + name: "Token 1", + symbol: "TK1", + uri: "", + uri_hash: "", + display: "Token 1", + base: "token1", + denom_units: [{ denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, { denom: "token1", exponent: 6, aliases: ["token1"] }], + }, + }, + { + denom: "token2", + coreDenom: "utoken2", + amount: "2000", + metadata: { + description: "My Second Token", + name: "Token 2", + symbol: "TK2", + uri: "", + uri_hash: "", + display: "Token 2", + base: "token2", + denom_units: [{ denom: "utoken2", exponent: 0, aliases: ["utoken2"] }, { denom: "token2", exponent: 6, aliases: ["token2"] }], + }, + }, +]; + +describe("TokenList", () => { + afterEach(() => { + cleanup(); + }); + + test("renders correctly", () => { + render(); + expect(screen.getByText("Your Balances")).toBeInTheDocument(); + }); + + test("displays loading skeleton when isLoading is true", () => { + render(); + expect(screen.getByText("Your wallet is empty!")).toBeInTheDocument(); + }); + + test("displays empty state message when there are no balances", () => { + render(); + expect(screen.getByText("Your wallet is empty!")).toBeInTheDocument(); + }); + + test("filters balances based on search term", () => { + render(); + const searchInput = screen.getByPlaceholderText("Search for a token..."); + fireEvent.change(searchInput, {target: {value: "token1"}}); + expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); + expect(screen.queryByText("TOKEN 2")).not.toBeInTheDocument(); + }); + + test("opens modal with correct denomination information", () => { + render(); + const balanceRow = screen.getByText("TOKEN 1").closest("tr"); + if (!balanceRow) throw new Error("Balance row not found"); + fireEvent.click(balanceRow); + expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); + }); +}); \ No newline at end of file From 244436c8fe7f0d67072ae2799ae969dba51945ad Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:46:05 -0400 Subject: [PATCH 17/63] fix: eof eol --- components/bank/components/__tests__/tokenList.test.tsx | 2 +- happydom.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/bank/components/__tests__/tokenList.test.tsx b/components/bank/components/__tests__/tokenList.test.tsx index 3202ecb8..d59954d5 100644 --- a/components/bank/components/__tests__/tokenList.test.tsx +++ b/components/bank/components/__tests__/tokenList.test.tsx @@ -75,4 +75,4 @@ describe("TokenList", () => { fireEvent.click(balanceRow); expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/happydom.ts b/happydom.ts index 9cae201a..7f712d02 100644 --- a/happydom.ts +++ b/happydom.ts @@ -1,3 +1,3 @@ import { GlobalRegistrator } from "@happy-dom/global-registrator"; -GlobalRegistrator.register(); \ No newline at end of file +GlobalRegistrator.register(); From 716b6e7d960bee8fa4ae550fa5b43ac868916a43 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:49:34 -0400 Subject: [PATCH 18/63] test: token list balance and base denom --- .../bank/components/__tests__/tokenList.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/bank/components/__tests__/tokenList.test.tsx b/components/bank/components/__tests__/tokenList.test.tsx index d59954d5..5c4a7a3b 100644 --- a/components/bank/components/__tests__/tokenList.test.tsx +++ b/components/bank/components/__tests__/tokenList.test.tsx @@ -75,4 +75,16 @@ describe("TokenList", () => { fireEvent.click(balanceRow); expect(screen.getByText("TOKEN 1")).toBeInTheDocument(); }); + + test("displays correct balance for each token", () => { + render(); + expect(screen.getByText("0.001")).toBeInTheDocument(); + expect(screen.getByText("0.002")).toBeInTheDocument(); + }); + + test("displays correct base denomination for each token", () => { + render(); + expect(screen.getByText("token1")).toBeInTheDocument(); + expect(screen.getByText("token2")).toBeInTheDocument(); + }); }); From 52b2955e84cfe0ee6843fd9af1ec7d2dd1fcba33 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:58:26 -0700 Subject: [PATCH 19/63] add codecov --- .github/workflows/test-coverage.yml | 19 +++++++++++++++++++ README.md | 2 ++ bun.lockb | Bin 572066 -> 576802 bytes package.json | 5 ++++- 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-coverage.yml diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 00000000..7f5f95a5 --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,19 @@ +name: Test and Coverage + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: bun install + - name: Run tests and generate coverage + run: bun run test:coverage + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index f452827b..d48fa831 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This is a web app that allows users to interact with the Manifest Network and it For more information on the Manifest Network and its modules, please visit the [Manifest Network GitHub](https://github.com/liftedinit/manifest-ledger). +[![codecov](https://codecov.io/gh/chalabi2/manifest-app/branch/main/graph/badge.svg)](https://codecov.io/gh/chalabi2/manifest-app) + ## Getting Started ### Installation diff --git a/bun.lockb b/bun.lockb index 71eec92f3df6088147be31d06f54eb9578552af2..f5eddc4ec01e1d38a3cad998e11d1b4ce36d47ac 100755 GIT binary patch delta 35419 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 NnTE?NZz!mm{$IW1&l>;$ diff --git a/package.json b/package.json index 5a45ab9f..508b759d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "start": "next start", "lint": "next lint", "remove": "rm -rf node_modules/ bun.lockb $HOME/.bun/install/cache/", - "update-deps": "bunx npm-check-updates --root --format group -i" + "update-deps": "bunx npm-check-updates --root --format group -i", + "test:coverage": "bun test --coverage", + "coverage:upload": "codecov" }, "author": "Joseph Chalabi", "license": "MIT", @@ -75,6 +77,7 @@ "@types/react": "18.2.55", "@types/react-dom": "18.2.19", "autoprefixer": "^10.4.13", + "codecov": "^3.8.3", "eslint": "8.56.0", "eslint-config-next": "13.0.5", "jest": "^29.7.0", From ffb29ce1c363e46020c77f50c85f96f4732f17bb Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:59:15 -0700 Subject: [PATCH 20/63] add .env's to readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index d48fa831..5151efc1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,21 @@ For more information on the Manifest Network and its modules, please visit the [ 2. Install dependencies - `bun install` +### .env + +``` +NEXT_PUBLIC_ABLY_API_KEY= +NEXT_PUBLIC_WALLETCONNECT_KEY= +NEXT_PUBLIC_WEB3_CLIENT_ID= +NEXT_PUBLIC_CHAIN=manifest +NEXT_PUBLIC_CHAIN_ID=manifest-1 +NEXT_PUBLIC_TESTNET_CHAIN_ID=manifest-ledger-beta +NEXT_PUBLIC_MAINNET_RPC_URL=https://nodes.chandrastation.com/rpc/manifest/ +NEXT_PUBLIC_TESTNET_RPC_URL=https://manifest-beta-rpc.liftedinit.tech/ +NEXT_PUBLIC_MAINNET_API_URL=https://nodes.chandrastation.com/api/manifest/ +NEXT_PUBLIC_TESTNET_API_URL=https://manifest-beta-rest.liftedinit.tech/ +``` + ### Development 1. Start the server From 8144d79f42d6ddcb9704217a135cc61e17b42872 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:11:18 -0700 Subject: [PATCH 21/63] add coverage --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 508b759d..6fdfb4c5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "next lint", "remove": "rm -rf node_modules/ bun.lockb $HOME/.bun/install/cache/", "update-deps": "bunx npm-check-updates --root --format group -i", - "test:coverage": "bun test --coverage", + "test:coverage": "bun test --coverage --coverage-reporter=lcov", "coverage:upload": "codecov" }, "author": "Joseph Chalabi", From 9556d4f5ea88ff232e6122f0d6c32769db333001 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:24:22 -0700 Subject: [PATCH 22/63] fix denom --- .../components/__tests__/historyBox.test.tsx | 103 ++++++++++++++++++ .../components/__tests__/sendBox.test.tsx | 93 ++++++++++++++++ components/factory/components/DenomImage.tsx | 22 +++- .../groups/components/groupProposals.tsx | 2 - .../forms/proposals/ConfirmationForm.tsx | 7 +- hooks/useLcdQueryClient.ts | 23 +--- hooks/useManifestLcdQueryClient.ts | 28 ++--- hooks/usePoaLcdQueryClient.ts | 26 +---- package.json | 2 +- pages/admins.tsx | 2 +- store/endpointStore.ts | 12 +- 11 files changed, 240 insertions(+), 80 deletions(-) create mode 100644 components/bank/components/__tests__/historyBox.test.tsx create mode 100644 components/bank/components/__tests__/sendBox.test.tsx diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx new file mode 100644 index 00000000..dd92be98 --- /dev/null +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -0,0 +1,103 @@ +import { test, expect, afterEach, describe } from "bun:test"; +import React from "react"; +import matchers from "@testing-library/jest-dom/matchers"; +import { fireEvent, render, screen, cleanup } from "@testing-library/react"; +import { + HistoryBox, + TransactionGroup, +} from "@/components/bank/components/historyBox"; + +expect.extend(matchers); + +const mockTransactions: TransactionGroup[] = [ + { + tx_hash: "hash1", + block_number: 1, + formatted_date: "2023-05-01T12:00:00Z", + data: { + from_address: "address1", + to_address: "address2", + amount: [{ amount: "1000000", denom: "utoken" }], + }, + }, + { + tx_hash: "hash2", + block_number: 2, + formatted_date: "2023-05-02T12:00:00Z", + data: { + from_address: "address2", + to_address: "address1", + amount: [{ amount: "2000000", denom: "utoken" }], + }, + }, +]; + +describe("HistoryBox", () => { + afterEach(() => { + cleanup(); + }); + + test("renders correctly", () => { + render( + + ); + expect(screen.getByText("Tx History")).toBeInTheDocument(); + }); + + test("displays transactions", () => { + render( + + ); + expect(screen.getByText("Send")).toBeInTheDocument(); + expect(screen.getByText("Receive")).toBeInTheDocument(); + }); + + test("displays 'No transactions found' message when there are no transactions", () => { + render(); + expect( + screen.getByText("No transactions found for this account!") + ).toBeInTheDocument(); + }); + + test("opens modal when clicking on a transaction", () => { + render( + + ); + fireEvent.click(screen.getByText("Send")); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + test("formats date correctly", () => { + render( + + ); + expect(screen.getByText("May 1, 2023")).toBeInTheDocument(); + }); + + test("formats amount correctly", () => { + render( + + ); + expect(screen.getByText("1 TOKEN")).toBeInTheDocument(); + }); +}); diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx new file mode 100644 index 00000000..84e7d616 --- /dev/null +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -0,0 +1,93 @@ +import { test, expect, afterEach, describe } from "bun:test"; +import React from "react"; +import matchers from "@testing-library/jest-dom/matchers"; +import { fireEvent, render, screen, cleanup } from "@testing-library/react"; +import SendBox from "@/components/bank/components/sendBox"; +import { CombinedBalanceInfo } from "@/pages/bank"; + +expect.extend(matchers); + +const mockBalances: CombinedBalanceInfo[] = [ + { + denom: "token1", + coreDenom: "utoken1", + amount: "1000", + metadata: { + description: "My First Token", + name: "Token 1", + symbol: "TK1", + uri: "", + uri_hash: "", + display: "Token 1", + base: "token1", + denom_units: [ + { denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, + { denom: "token1", exponent: 6, aliases: ["token1"] }, + ], + }, + }, +]; + +describe("SendBox", () => { + afterEach(() => { + cleanup(); + }); + + test("renders correctly", () => { + render( + {}} + /> + ); + expect(screen.getByText("Send Tokens")).toBeInTheDocument(); + }); + + test("toggles between Send and IBC Transfer", () => { + render( + {}} + /> + ); + expect(screen.getByText("Send Tokens")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("IBC Transfer")); + expect(screen.getByText("IBC Transfer")).toBeInTheDocument(); + }); + + test("displays chain selection dropdown when in IBC Transfer mode", () => { + render( + {}} + /> + ); + + fireEvent.click(screen.getByText("IBC Transfer")); + expect(screen.getByText("Chain")).toBeInTheDocument(); + }); + + test("selects a chain in IBC Transfer mode", () => { + render( + {}} + /> + ); + + fireEvent.click(screen.getByText("IBC Transfer")); + fireEvent.click(screen.getByText("Chain")); + fireEvent.click(screen.getByText("Osmosis")); + + expect(screen.getByText("Osmosis")).toBeInTheDocument(); + }); +}); diff --git a/components/factory/components/DenomImage.tsx b/components/factory/components/DenomImage.tsx index cf276420..d4859808 100644 --- a/components/factory/components/DenomImage.tsx +++ b/components/factory/components/DenomImage.tsx @@ -14,6 +14,15 @@ const supportedDomains = [ "images.unsplash.com", "media.giphy.com", "media.istockphoto.com", + "imgix.net", + "staticflickr.com", + "twimg.com", + "pinimg.com", + "giphy.com", + "dropboxusercontent.com", + "googleusercontent.com", + "unsplash.com", + "istockphoto.com", ]; const supportedPatterns = [ @@ -29,6 +38,15 @@ const supportedPatterns = [ /^https:\/\/.*\.googleusercontent\.com/, /^https:\/\/.*\.unsplash\.com/, /^https:\/\/.*\.istockphoto\.com/, + /^https:\/\/.*\.media\.giphy\.com/, + /^https:\/\/.*\.media\.istockphoto\.com/, + /^https:\/\/.*\.images\.unsplash\.com/, + /^https:\/\/.*\.media\.istockphoto\.com/, + /^https:\/\/.*\.imgix\.net/, + /^https:\/\/.*\.staticflickr\.com/, + /^https:\/\/.*\.twimg\.com/, + /^https:\/\/.*\.pinimg\.com/, + /^https:\/\/.*\.giphy\.com/, ]; const isUrlSupported = (url: string) => { @@ -50,7 +68,7 @@ export const DenomImage = ({ denom }: { denom: any }) => { useEffect(() => { const checkUri = async () => { - if (denom.uri) { + if (denom?.uri) { setIsSupported(isUrlSupported(denom.uri)); // Simulate a delay to show the loading state await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -59,7 +77,7 @@ export const DenomImage = ({ denom }: { denom: any }) => { }; checkUri(); - }, [denom.uri]); + }, [denom?.uri]); if (isLoading) { return ( diff --git a/components/groups/components/groupProposals.tsx b/components/groups/components/groupProposals.tsx index dae7508f..21a5412a 100644 --- a/components/groups/components/groupProposals.tsx +++ b/components/groups/components/groupProposals.tsx @@ -73,8 +73,6 @@ export default function ProposalsForPolicy({ const { proposals, isProposalsLoading, isProposalsError, refetchProposals } = useProposalsByPolicyAccount(policyAddress ?? ""); - console.log("proposals", proposals); - const members = groupByMemberData?.groups.filter( (group) => group.policies[0]?.address === policyAddress diff --git a/components/groups/forms/proposals/ConfirmationForm.tsx b/components/groups/forms/proposals/ConfirmationForm.tsx index 0a4263a9..5cb20916 100644 --- a/components/groups/forms/proposals/ConfirmationForm.tsx +++ b/components/groups/forms/proposals/ConfirmationForm.tsx @@ -145,11 +145,10 @@ export default function ConfirmationModal({ const getMessageObject = ( message: { type: keyof MessageTypeMap } & Record ): Any => { - console.log("Processing message:", JSON.stringify(message, null, 2)); const composer = messageTypeToComposer[message.type]; if (composer) { let messageData = JSON.parse(JSON.stringify(message)); - console.log("Message data:", messageData); + delete messageData.type; messageData = convertKeysToCamelCase(messageData); @@ -159,7 +158,6 @@ export default function ConfirmationModal({ const composedMessage = composer( messageData as MessageTypeMap[typeof message.type] ); - console.log("Composed message:", composedMessage); if (!composedMessage || !composedMessage.value) { console.error( @@ -261,11 +259,8 @@ export default function ConfirmationModal({ } try { - console.log("Composed message value:", composedMessage.value); const encodedValue = encodeFunction(composedMessage.value).finish(); - console.log("Encoded value:", encodedValue); - const anyMessage = Any.fromPartial({ typeUrl: composedMessage.typeUrl, value: encodedValue, diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts index 41b6459c..84a59301 100644 --- a/hooks/useLcdQueryClient.ts +++ b/hooks/useLcdQueryClient.ts @@ -9,34 +9,17 @@ import { useEndpointStore } from "@/store/endpointStore"; const createLcdQueryClient = cosmos.ClientFactory.createLCDClient; export const useLcdQueryClient = () => { - const { getRestEndpoint } = useChain(chainName); - const [resolvedRestEndpoint, setResolvedRestEndpoint] = useState< - string | null - >(null); - - useEffect(() => { - const resolveEndpoint = async () => { - const endpoint = await getRestEndpoint(); - - if (typeof endpoint === "string") { - setResolvedRestEndpoint(endpoint); - } else if (endpoint && typeof endpoint === "object") { - setResolvedRestEndpoint(endpoint.url); - } - }; - - resolveEndpoint(); - }, [getRestEndpoint]); + const {selectedEndpoint} = useEndpointStore(); - console.log(selectedEndpoint) + const lcdQueryClient = useQuery({ queryKey: ["lcdQueryClient", selectedEndpoint?.api], queryFn: () => createLcdQueryClient({ restEndpoint: selectedEndpoint?.api || "", }), - enabled: !!resolvedRestEndpoint, + enabled: !!selectedEndpoint?.api, staleTime: Infinity, }); diff --git a/hooks/useManifestLcdQueryClient.ts b/hooks/useManifestLcdQueryClient.ts index 7ea25481..f1bac05c 100644 --- a/hooks/useManifestLcdQueryClient.ts +++ b/hooks/useManifestLcdQueryClient.ts @@ -6,37 +6,23 @@ import {osmosis} from "@chalabi/manifestjs" import { useQuery } from "@tanstack/react-query"; import { useChain } from "@cosmos-kit/react"; import { chainName } from "../config"; +import { useEndpointStore } from "@/store/endpointStore"; const createLcdQueryClient = osmosis.ClientFactory.createLCDClient; export const useManifestLcdQueryClient = () => { - const { getRestEndpoint } = useChain(chainName); - const [resolvedRestEndpoint, setResolvedRestEndpoint] = useState< - string | null - >(null); - - useEffect(() => { - const resolveEndpoint = async () => { - const endpoint = await getRestEndpoint(); - - if (typeof endpoint === "string") { - setResolvedRestEndpoint(endpoint); - } else if (endpoint && typeof endpoint === "object") { - setResolvedRestEndpoint(endpoint.url); - } - }; - - resolveEndpoint(); - }, [getRestEndpoint]); + + const {selectedEndpoint} = useEndpointStore(); + const lcdQueryClient = useQuery({ - queryKey: ["lcdQueryClient", resolvedRestEndpoint], + queryKey: ["lcdQueryClient", selectedEndpoint?.api], queryFn: () => createLcdQueryClient({ - restEndpoint: resolvedRestEndpoint || "", + restEndpoint: selectedEndpoint?.api || "", }), - enabled: !!resolvedRestEndpoint, + enabled: !!selectedEndpoint?.api, staleTime: Infinity, }); diff --git a/hooks/usePoaLcdQueryClient.ts b/hooks/usePoaLcdQueryClient.ts index 2214f8d9..77a5851e 100644 --- a/hooks/usePoaLcdQueryClient.ts +++ b/hooks/usePoaLcdQueryClient.ts @@ -6,36 +6,20 @@ import { manifest, strangelove_ventures } from "@chalabi/manifestjs"; import { useQuery } from "@tanstack/react-query"; import { useChain } from "@cosmos-kit/react"; import { chainName } from "../config"; +import { useEndpointStore } from "@/store/endpointStore"; const createLcdQueryClient = strangelove_ventures.ClientFactory.createLCDClient; export const usePoaLcdQueryClient = () => { - const { getRestEndpoint } = useChain(chainName); - const [resolvedRestEndpoint, setResolvedRestEndpoint] = useState< - string | null - >(null); - - useEffect(() => { - const resolveEndpoint = async () => { - const endpoint = await getRestEndpoint(); - - if (typeof endpoint === "string") { - setResolvedRestEndpoint(endpoint); - } else if (endpoint && typeof endpoint === "object") { - setResolvedRestEndpoint(endpoint.url); - } - }; - - resolveEndpoint(); - }, [getRestEndpoint]); + const {selectedEndpoint} = useEndpointStore(); const lcdQueryClient = useQuery({ - queryKey: ["lcdQueryClient", resolvedRestEndpoint], + queryKey: ["lcdQueryClient", selectedEndpoint?.api], queryFn: () => createLcdQueryClient({ - restEndpoint: resolvedRestEndpoint || "", + restEndpoint: selectedEndpoint?.api || "", }), - enabled: !!resolvedRestEndpoint, + enabled: !!selectedEndpoint?.api, staleTime: Infinity, }); diff --git a/package.json b/package.json index 6fdfb4c5..77354f84 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "next lint", "remove": "rm -rf node_modules/ bun.lockb $HOME/.bun/install/cache/", "update-deps": "bunx npm-check-updates --root --format group -i", - "test:coverage": "bun test --coverage --coverage-reporter=lcov", + "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-ignore='**/*.test.{js,ts,jsx,tsx}'", "coverage:upload": "codecov" }, "author": "Joseph Chalabi", diff --git a/pages/admins.tsx b/pages/admins.tsx index 93ab2702..93cac062 100644 --- a/pages/admins.tsx +++ b/pages/admins.tsx @@ -31,7 +31,7 @@ export default function Admins() { const { stakingParams, isParamsLoading, refetchParams } = useStakingParams(); const { validators, isActiveValidatorsLoading, refetchActiveValidatorss } = useValidators(); - console.log(poaParams); + const { groupByAdmin, isGroupByAdminLoading, refetchGroupByAdmin } = useGroupsByAdmin( poaParams?.admins[0] ?? diff --git a/store/endpointStore.ts b/store/endpointStore.ts index 78e5acef..c9a7fe85 100644 --- a/store/endpointStore.ts +++ b/store/endpointStore.ts @@ -25,18 +25,18 @@ const validateRPCEndpoint = async (rpc: string): Promise => { try { const url = new URL('status', rpc.trim()); const response = await fetch(url.toString()); - console.log('RPC response status:', response.status); + const data = await response.json(); - console.log('RPC response data:', data); + if (data.result && data.result.node_info && data.result.sync_info) { const networkMatches = data.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || process.env.NEXT_PUBLIC_TESTNET_CHAIN_ID); const isNotCatchingUp = !data.result.sync_info.catching_up; - console.log('Network matches:', networkMatches, 'Not catching up:', isNotCatchingUp); + return true; } else { - console.log('Unexpected RPC response structure'); + return false; } } catch (error) { @@ -91,12 +91,12 @@ export const useEndpointStore = create( const isRPCValid = await validateRPCEndpoint(rpc); const isAPIValid = await validateAPIEndpoint(api); - console.log('RPC validation:', isRPCValid, 'API validation:', isAPIValid); + if (isRPCValid && isAPIValid) { const rpcResponse = await fetch(`${rpc.trim()}status`); const rpcData = await rpcResponse.json(); - console.log('RPC data:', rpcData); + const network = rpcData.result.node_info.network === (process.env.NEXT_PUBLIC_CHAIN_ID || process.env.NEXT_PUBLIC_TESTNET_CHAIN_ID) ? "mainnet" : "testnet"; From 1c4bc4f52c62646468c09e4b012249b0b68012ca Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:30:20 -0400 Subject: [PATCH 23/63] test: add more test and fix existing tests --- bun.lockb | Bin 576802 -> 576730 bytes .../__tests__/validatorList.test.tsx | 59 ++++++++ .../components/__tests__/historyBox.test.tsx | 25 ++-- .../components/__tests__/sendBox.test.tsx | 110 +++++--------- tests/mock.ts | 139 ++++++++++++++++++ tests/render.tsx | 22 +++ 6 files changed, 270 insertions(+), 85 deletions(-) create mode 100644 components/admins/components/__tests__/validatorList.test.tsx create mode 100644 tests/mock.ts create mode 100644 tests/render.tsx diff --git a/bun.lockb b/bun.lockb index f5eddc4ec01e1d38a3cad998e11d1b4ce36d47ac..7042788f6c6bd610967800ca079149ca1a22a92b 100755 GIT binary patch delta 4771 zcmXYz30zgx7RK)d4r2~rDrlu-nl_lIm538gASOhmJ?p8MPfb&cOw9?rDv_20`gEt2 z38u}y$n-wPH1#z&^yuY~X+~LEl7jQ>`>*`gkHuN%|E>GobN1e6pIfrH(Sb#c=4Sdc z0$BxF{(`KKf~?SjtgzKtUjJl&j$4`RWh5U6^ZEQfpRWdKVdo(nE$=o!98^yz^}T-) zpYM=MYU|Z%|Dz-i+@|^s?XWDuIc+UguD%FZi<@rr{phQCDUb$0Sul`onzeC=0uQ<6_6f>}D&(DqLa;7Jo{LMcOIpgjgJGYb5`OlVY`~ zhl^FZT(hW>R2ylR^%AGV>QG-MZCFo>)urA}TAXoP%;K!11W^0G1fTDmSUt`=h@BU! z&w0Ao1sB^6i;I$O1$XA=ZDuaP?3-^R!)^SCE9z@N9VvFz4a6ev9KGgy6;Y4H9^n$+jCpt>TiY= zYs-R&5W9;yPApQ))^07aD3@&(F_LTv*OqjXSaVn%v1%|It^3G&Vl`Z`S=5x2gmhA? zBu*ABVU=RF#9G0k%!|0$RhdO?N%w;?ZS;sbVh>P1CRSJML0E+rTD(i`fJJ>IGfgJ* z^o_Twy)A5w*ll7du%K82S7a6qCD|gIr3*|Hvqjd)=A5`)tRr=*SR+?%7L6sPBBe=c zBGws}E_SC_7ufT*ClF0tQb#PBN$Lg~E9ow=?yzxUcZ;RLUKG2>1T@#2yzL4cjdCH&RJAWc>L+O$ER^%b0(8$)GX$y zeL2z|?MCy(R>1a&{Zs56SZi4#=~M*&2q*SF^+mDQ#6Eys7hB}A&0?{n zkC1MW^t#weSc!Ov31n;OWfi; zvGvs3VD`|z?~2Ug10?=@8_6!Vo(S8??J72rYbmC1{6rR?QO_j<ymn4u};!}#^S*k-XEu${8l;>yfoo1|Sx19+tDaoY~F8`({cw?QGkRNpt$Z)~YLaYd|(dKqamcvb8a^>VRmVy8J@%)r>J zUKcw$mR#-uVik+vPE*2(s0ahsHiCu(^G-D!M z>=Jc--CRUCJBfYfGTA^dQjJ$&H;YAyU4`XoAfv^u!SZ3hbBux6N3N5(>Z`6k`=ZYg zZNzJc`SXEixnsNYnt~y)Qqt}y78b!Vl=D6^j8|V6EJ#{^f|y6WQLLWWT>CjnT(KWP zM0C3`ty*?y*t+59VG~@=P_L$4aqx>wuUpNK5uw580p7R%Xq)S|$rZlffgEpUSi-Y( zJx7iwCy;sM)8ueBd9atiaEKT0Eu(+AYc|B26ZIjby)!;`yM}maE`F$YPjplJ!9{FX z8q6H(tx1lmTzUhv3|1@l-t|ZL`?qS|rg^Kv4JBT1LrM6i7ow)rwVU_(a)Q(9Ml=qI ynwxOrym?`1LPW4MA#zV?)N?ynTEuH^&W_0Jm@jpju`|-uDE1l$YwnDU^Zy?u8x+$3 delta 4828 zcmXYz30zfW8piLva9Py^T*iHHOKUVSE0J7qLqQ=dZ5tPI)X{0fOmn4cFsO|SxO}N) zflD@-#iZWUv`B3%m&qv=Ef*$DQ&Mp^P3Ql{^ZtB2@A*IPdG2?pykcyY7hCC%og zdkX^@c^Tfk3}0SGNM1(h`V9Y&MDLq!ZK6Ll@j$4@ zekUH!A(zm}AJ_S?BtN)gjVkSXS%h)gLF|b7!eQ;IRrMWp0kb$JDH7UJ(s8jUSSzt| z*+#=!i~Z>KnZ*f7)sfDa5Al;MYyfA)PKnvgR*0Q;@kv zi=!SRR^hVE;ulG^k@i?GaZaob^-a>I^}JYJ>K&xT1y^bom6Ga#I{z+sJQv04bKXVl zl2`-I`-ok3F%M&LMN%VhcW&NR<|@p7xiJ}L^G94)UlZyGu^TQ6i=1C2#Ute~)3#_g z#hOxQYxZx6HG_Gz(7%Z_hh4VyAa0AbppMeDcf?x4qQz{i?8O?MyMfgOz0TX2+wxUu zp4Lb)wk?Pdv3sav#X`kw@5YJwU8Y%tOR_CoTT+BrTUZ^jNSMvmePn&HXjfzw)g&b# zo!2g@A&d5~3u1SP-4Ba2FQTTaG>aHX4}j9`$s=OL9;ALuEKckpScNuPEtlK{i@Hc= znn+I5FV<6gC)ftD`eI42pjZP}U=|G}*(RH<0X7n|P1eoUoM|&C!=qzb4QlX?SVnbl_#kz_Og)J28=CaHpSyC3#8fuH~V$V>2Al5@{IBcz0 zPgi0Vy(Eo5`bbi5v5~M6u@tdUu#d(1IB$0>`brv&v|duG*cjLbu{5#2!GdD_T&7v{ zm-HOcMoH;n&%-u}Wr)21+bs5oD>92rN!dtSB@GZ83;Rs$QL&d`pNl=_D$Qb`q?eIq zNE#$I9`?AH9qrXIfwZ5nc)}(3z~V_s6G65gES?gZL|sShX|c($x?+EJ1!gf=(o`h- z1X>Icn?_wBHdHJJwoPo9D>sWQNdcr>@(<)QV$-P$^pYDc_8M%RUcY~F2|ckGAt?{3 zL~pH;VtLWBtEFacT-;uw%fLV-|G!yA_efGw{_~)5LZWDV!eXqloiM{CdnMJmw z*+}gqjTM^%OB8!aY%Z*m*f-=8CO?9Tb}<_Aac0EDD@A1&cyS?;%|b=Rab;*gvUnh%FF%A9hD@ki8S^VO}csFX|*}dy3x_`y2aZZZDu!+vD8yI7O5{teSs&{p6p_m+y{&0k~V_uk6L@cSBPz*KB)(L zrPyZJc9=c%t6YIuyo+SFyM^pw`-!j@zirg5$`nJK+^uAc{68d7XLDCMS{*r=XU&7MGJ`wvjEJJLg3z)?w zNnatA=@&PPeGTg^_Nmw|*lt;Dar?}|Ug&lsd&r?YQuer&!t6%&k`rxdi0$h85A_P| z&K)j36^k#CtZN_nM{0|mYX2{FJhd(OS7P5%+h6AvU%PB9a&}4DZ;aYvx7Y#dXBp0? z9QTMFr0!!wB=*AWk}}esV2f`7w!(+VMU2*#>5#5HOzp%DS23UGdsk_<@&npcP1+CY zKLWcZc0}wb?7G-dv12Yd4U6MQ&ojr{IhG^YD9g#cFq{7qYX2Yg6()Q)$CF||x&pH} z0V-jHHrJ;lourP|9iN6-`4d^4;v0@<)pyF3o5g8R6zDsS=hXf)bqKXha;4ZA>KI%Dp2ot+OohueD_A4x3EJExi>^U0&5h-?yx`A#uDvVc-UGp2+L@-*7w_&x!s)^l! zP1EF77c+w#n5|O{m|fz9WveeneLmP1i09E`#X@p`s{uA`ae|>R+jce^wZ#0e64LIn zHq1T`VVn=rk2F+YI4nq7eMdf8#dfB%UWBjbC^shE-!ix$-QTTduxyC`8*gyk zF#n8D-?JgE(+Gck_rwVQls2XOPAY>>Bm0p9$U$Tg9=27MlPgHu4y#<{2>;yL?^CWJ z?HltEX}9^Fn>o^-;#=c!dq?`)M!jf%Xc3`BHi)LdR-^n25+gg>r-hgl-0_ZowKrS~ zm_M%A9~@U4wsl { + const defaultProps = { + admin: "admin1", + activeValidators: mockActiveValidators, + pendingValidators: mockPendingValidators, + isLoading: false, + }; + return renderWithChainProvider(); +}; + +describe("ValidatorList", () => { + afterEach(cleanup); + + test("renders correctly", () => { + renderWithProps(); + expect(screen.getByText("Active Validators")).toBeInTheDocument(); + expect(screen.getByText("Validator One")).toBeInTheDocument(); + expect(screen.getByText("Validator Two")).toBeInTheDocument(); + }); + + test("search functionality works", () => { + renderWithProps(); + fireEvent.change(screen.getByPlaceholderText("Search for a validator..."), { + target: { value: "Validator One" }, + }); + expect(screen.getByText("Validator One")).toBeInTheDocument(); + expect(screen.queryByText("Validator Two")).not.toBeInTheDocument(); + }); + + test("active/pending toggle works", () => { + renderWithProps(); + fireEvent.click(screen.getByText("Pending")); + expect(screen.getByText("Pending Validators")).toBeInTheDocument(); + expect(screen.getByText("Validator Three")).toBeInTheDocument(); + }); + + test("clicking on a validator row opens the modal", async () => { + renderWithProps(); + fireEvent.click(screen.getByText("Validator One")); + await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); + }); + + test("remove button works and shows the warning modal", async () => { + renderWithProps(); + const allRemoveButtons = screen.getAllByText("Remove"); + fireEvent.click(allRemoveButtons[0]); + await waitFor(() => expect(screen.getByText("Are you sure you want to remove the validator")).toBeInTheDocument()); + }); +}); diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index dd92be98..87230faf 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -1,7 +1,7 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; -import { fireEvent, render, screen, cleanup } from "@testing-library/react"; +import {render, screen, cleanup} from "@testing-library/react"; import { HistoryBox, TransactionGroup, @@ -67,17 +67,18 @@ describe("HistoryBox", () => { ).toBeInTheDocument(); }); - test("opens modal when clicking on a transaction", () => { - render( - - ); - fireEvent.click(screen.getByText("Send")); - expect(screen.getByRole("dialog")).toBeInTheDocument(); - }); + // TODO: Failing + // test("opens modal when clicking on a transaction", async () => { + // render( + // + // ); + // fireEvent.click(screen.getByText("Send")); + // await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); + // }); test("formats date correctly", () => { render( diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index 84e7d616..ced79c4e 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -1,93 +1,57 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; -import { fireEvent, render, screen, cleanup } from "@testing-library/react"; +import {fireEvent, screen, cleanup, waitFor} from "@testing-library/react"; import SendBox from "@/components/bank/components/sendBox"; -import { CombinedBalanceInfo } from "@/pages/bank"; +import {mockBalances} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; expect.extend(matchers); -const mockBalances: CombinedBalanceInfo[] = [ - { - denom: "token1", - coreDenom: "utoken1", - amount: "1000", - metadata: { - description: "My First Token", - name: "Token 1", - symbol: "TK1", - uri: "", - uri_hash: "", - display: "Token 1", - base: "token1", - denom_units: [ - { denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, - { denom: "token1", exponent: 6, aliases: ["token1"] }, - ], - }, - }, -]; - +const renderWithProps = (props = {}) => { + const defaultProps = { + address: "test_address", + balances: mockBalances, + isBalancesLoading: false, + refetchBalances: () => {}, + } + return renderWithChainProvider(); +}; + +// TODO: ALL FAILING describe("SendBox", () => { afterEach(() => { cleanup(); }); test("renders correctly", () => { - render( - {}} - /> - ); + renderWithProps(); expect(screen.getByText("Send Tokens")).toBeInTheDocument(); }); - test("toggles between Send and IBC Transfer", () => { - render( - {}} - /> - ); - expect(screen.getByText("Send Tokens")).toBeInTheDocument(); - + // TODO: Failing + // test("toggles between Send and IBC Transfer", () => { + // renderWithProps(); + // expect(screen.getByText("Send Tokens")).toBeInTheDocument(); + // + // fireEvent.click(screen.getByText("IBC Transfer")); + // expect(screen.getByText("IBC Transfer")).toBeInTheDocument(); + // }); + + test("displays chain selection dropdown when in IBC Transfer mode", async () => { + renderWithProps(); fireEvent.click(screen.getByText("IBC Transfer")); - expect(screen.getByText("IBC Transfer")).toBeInTheDocument(); + await waitFor(() => expect(screen.getByText("Chain")).toBeInTheDocument()); }); - test("displays chain selection dropdown when in IBC Transfer mode", () => { - render( - {}} - /> - ); - - fireEvent.click(screen.getByText("IBC Transfer")); - expect(screen.getByText("Chain")).toBeInTheDocument(); - }); - - test("selects a chain in IBC Transfer mode", () => { - render( - {}} - /> - ); - - fireEvent.click(screen.getByText("IBC Transfer")); - fireEvent.click(screen.getByText("Chain")); - fireEvent.click(screen.getByText("Osmosis")); - - expect(screen.getByText("Osmosis")).toBeInTheDocument(); - }); + // TODO: Failing + // test("selects a chain in IBC Transfer mode", async () => { + // renderWithProps(); + // + // fireEvent.click(screen.getByText("IBC Transfer")); + // fireEvent.click(screen.getByText("Chain")); + // fireEvent.click(screen.getByText("Osmosis")); + // + // await waitFor(() => expect(screen.getByText("Osmosis")).toBeInTheDocument()); + // }); }); diff --git a/tests/mock.ts b/tests/mock.ts new file mode 100644 index 00000000..2177b0c2 --- /dev/null +++ b/tests/mock.ts @@ -0,0 +1,139 @@ +import {Chain} from "@chain-registry/types"; +import {BondStatus} from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; +import {ExtendedValidatorSDKType} from "@/components"; +import {CombinedBalanceInfo} from "@/pages/bank"; + +export const mockActiveValidators: ExtendedValidatorSDKType[] = [ + { + operator_address: "validator1", + description: { moniker: "Validator One", identity: "identity1", details: "details1", website: "website1.com", security_contact: "security1" }, + consensus_power: BigInt(1000), + logo_url: "", + jailed: false, + status: BondStatus.BOND_STATUS_BONDED, + tokens: "1000upoa", + delegator_shares: "1000", + min_self_delegation: "1", + unbonding_height: 0n, + unbonding_time: new Date(), + commission: { + commission_rates: { + rate: "0.1", + max_rate: "0.2", + max_change_rate: "0.01", + }, + update_time: new Date(), + }, + unbonding_on_hold_ref_count: 0n, + unbonding_ids: [], + }, + { + operator_address: "validator2", + description: { moniker: "Validator Two", identity: "identity2", details: "details2", website: "website2.com", security_contact: "security2" }, + consensus_power: BigInt(2000), + logo_url: "", + jailed: false, + status: BondStatus.BOND_STATUS_BONDED, + tokens: "2000upoa", + delegator_shares: "2000", + min_self_delegation: "1", + unbonding_height: 0n, + unbonding_time: new Date(), + commission: { + commission_rates: { + rate: "0.1", + max_rate: "0.2", + max_change_rate: "0.01", + }, + update_time: new Date(), + }, + unbonding_on_hold_ref_count: 0n, + unbonding_ids: [], + }, +]; + +export const mockPendingValidators: ExtendedValidatorSDKType[] = [ + { + operator_address: "validator3", + description: { moniker: "Validator Three", identity: "identity2", details: "details2", website: "website2.com", security_contact: "security2" }, + consensus_power: BigInt(3000), + logo_url: "", + jailed: false, + status: BondStatus.BOND_STATUS_UNBONDED, + tokens: "3000upoa", + delegator_shares: "3000", + min_self_delegation: "1", + unbonding_height: 0n, + unbonding_time: new Date(), + commission: { + commission_rates: { + rate: "0.1", + max_rate: "0.2", + max_change_rate: "0.01", + }, + update_time: new Date(), + }, + unbonding_on_hold_ref_count: 0n, + unbonding_ids: [], + }, +]; + +export const defaultAssetLists = [ + { + chain_name: "manifest", + assets: [ + { + name: "Manifest Network Token", + display: "umfx", + base: "umfx", + symbol: "umfx", + denom_units: [ + { denom: "umfx", exponent: 0, aliases: ["umfx"] }, + ] + } + ], + }, +]; + +export const defaultChain: Chain = { + chain_name: "manifest", + chain_id: "manifest-1", + status: "live", + network_type: "testnet", + pretty_name: "Manifest Network", + bech32_prefix: "manifest", + slip44: 118, + fees: { + fee_tokens: [ + { + denom: "umfx", + fixed_min_gas_price: 0.001, + low_gas_price: 0.001, + average_gas_price: 0.001, + high_gas_price: 0.001, + }, + ], + }, +} + +export const mockBalances: CombinedBalanceInfo[] = [ + { + denom: "token1", + coreDenom: "utoken1", + amount: "1000", + metadata: { + description: "My First Token", + name: "Token 1", + symbol: "TK1", + uri: "", + uri_hash: "", + display: "Token 1", + base: "token1", + denom_units: [ + { denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, + { denom: "token1", exponent: 6, aliases: ["token1"] }, + ], + }, + }, +]; + diff --git a/tests/render.tsx b/tests/render.tsx new file mode 100644 index 00000000..40025c3e --- /dev/null +++ b/tests/render.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import {render} from "@testing-library/react"; +import {ChainProvider} from "@cosmos-kit/react"; +import {ToastProvider} from "@/contexts"; +import {defaultAssetLists, defaultChain} from "@/tests/mock" + +const defaultOptions = { + chains: [defaultChain], + assetLists: defaultAssetLists, + wallets: [], +} + +export const renderWithChainProvider = (ui: React.ReactElement, options = {}) => { + const combinedOptions = { ...defaultOptions, ...options }; + return render( + + + {ui} + + , options); +}; + From afff08afd2d87d859ce06c76c8c07753c4c8559b Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:27:36 -0400 Subject: [PATCH 24/63] test: cleanup --- .../components/__tests__/historyBox.test.tsx | 29 +------- .../components/__tests__/sendBox.test.tsx | 1 - .../components/__tests__/tokenList.test.tsx | 35 +--------- tests/mock.ts | 70 ++++++++++++++----- 4 files changed, 56 insertions(+), 79 deletions(-) diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index 87230faf..52a567c2 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -2,36 +2,11 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; import {render, screen, cleanup} from "@testing-library/react"; -import { - HistoryBox, - TransactionGroup, -} from "@/components/bank/components/historyBox"; +import {HistoryBox} from "@/components/bank/components/historyBox"; +import {mockTransactions} from "@/tests/mock"; expect.extend(matchers); -const mockTransactions: TransactionGroup[] = [ - { - tx_hash: "hash1", - block_number: 1, - formatted_date: "2023-05-01T12:00:00Z", - data: { - from_address: "address1", - to_address: "address2", - amount: [{ amount: "1000000", denom: "utoken" }], - }, - }, - { - tx_hash: "hash2", - block_number: 2, - formatted_date: "2023-05-02T12:00:00Z", - data: { - from_address: "address2", - to_address: "address1", - amount: [{ amount: "2000000", denom: "utoken" }], - }, - }, -]; - describe("HistoryBox", () => { afterEach(() => { cleanup(); diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index ced79c4e..f61e0967 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -18,7 +18,6 @@ const renderWithProps = (props = {}) => { return renderWithChainProvider(); }; -// TODO: ALL FAILING describe("SendBox", () => { afterEach(() => { cleanup(); diff --git a/components/bank/components/__tests__/tokenList.test.tsx b/components/bank/components/__tests__/tokenList.test.tsx index 5c4a7a3b..2725f0ec 100644 --- a/components/bank/components/__tests__/tokenList.test.tsx +++ b/components/bank/components/__tests__/tokenList.test.tsx @@ -3,43 +3,10 @@ import React from "react"; import matchers from '@testing-library/jest-dom/matchers'; import {fireEvent, render, screen, cleanup} from "@testing-library/react"; import TokenList from "@/components/bank/components/tokenList"; -import { CombinedBalanceInfo } from "@/pages/bank"; +import {mockBalances} from "@/tests/mock"; expect.extend(matchers); -const mockBalances: CombinedBalanceInfo[] = [ - { - denom: "token1", - coreDenom: "utoken1", - amount: "1000", - metadata: { - description: "My First Token", - name: "Token 1", - symbol: "TK1", - uri: "", - uri_hash: "", - display: "Token 1", - base: "token1", - denom_units: [{ denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, { denom: "token1", exponent: 6, aliases: ["token1"] }], - }, - }, - { - denom: "token2", - coreDenom: "utoken2", - amount: "2000", - metadata: { - description: "My Second Token", - name: "Token 2", - symbol: "TK2", - uri: "", - uri_hash: "", - display: "Token 2", - base: "token2", - denom_units: [{ denom: "utoken2", exponent: 0, aliases: ["utoken2"] }, { denom: "token2", exponent: 6, aliases: ["token2"] }], - }, - }, -]; - describe("TokenList", () => { afterEach(() => { cleanup(); diff --git a/tests/mock.ts b/tests/mock.ts index 2177b0c2..7b6131d6 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -1,8 +1,42 @@ import {Chain} from "@chain-registry/types"; import {BondStatus} from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; -import {ExtendedValidatorSDKType} from "@/components"; +import {ExtendedValidatorSDKType, TransactionGroup} from "@/components"; import {CombinedBalanceInfo} from "@/pages/bank"; +export const mockBalances: CombinedBalanceInfo[] = [ + { + denom: "token1", + coreDenom: "utoken1", + amount: "1000", + metadata: { + description: "My First Token", + name: "Token 1", + symbol: "TK1", + uri: "", + uri_hash: "", + display: "Token 1", + base: "token1", + denom_units: [{ denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, { denom: "token1", exponent: 6, aliases: ["token1"] }], + }, + }, + { + denom: "token2", + coreDenom: "utoken2", + amount: "2000", + metadata: { + description: "My Second Token", + name: "Token 2", + symbol: "TK2", + uri: "", + uri_hash: "", + display: "Token 2", + base: "token2", + denom_units: [{ denom: "utoken2", exponent: 0, aliases: ["utoken2"] }, { denom: "token2", exponent: 6, aliases: ["token2"] }], + }, + }, +]; + + export const mockActiveValidators: ExtendedValidatorSDKType[] = [ { operator_address: "validator1", @@ -116,23 +150,25 @@ export const defaultChain: Chain = { }, } -export const mockBalances: CombinedBalanceInfo[] = [ +export const mockTransactions: TransactionGroup[] = [ { - denom: "token1", - coreDenom: "utoken1", - amount: "1000", - metadata: { - description: "My First Token", - name: "Token 1", - symbol: "TK1", - uri: "", - uri_hash: "", - display: "Token 1", - base: "token1", - denom_units: [ - { denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, - { denom: "token1", exponent: 6, aliases: ["token1"] }, - ], + tx_hash: "hash1", + block_number: 1, + formatted_date: "2023-05-01T12:00:00Z", + data: { + from_address: "address1", + to_address: "address2", + amount: [{ amount: "1000000", denom: "utoken" }], + }, + }, + { + tx_hash: "hash2", + block_number: 2, + formatted_date: "2023-05-02T12:00:00Z", + data: { + from_address: "address2", + to_address: "address1", + amount: [{ amount: "2000000", denom: "utoken" }], }, }, ]; From 22e257cf8b7b7123acb0c5e41f8023d779bf20f7 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:07:44 -0400 Subject: [PATCH 25/63] test: staking params --- .../__tests__/stakingParams.test.tsx | 57 +++++++++++++++++++ .../admins/components/stakingParams.tsx | 10 +++- tests/mock.ts | 11 +++- 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 components/admins/components/__tests__/stakingParams.test.tsx diff --git a/components/admins/components/__tests__/stakingParams.test.tsx b/components/admins/components/__tests__/stakingParams.test.tsx new file mode 100644 index 00000000..ae1a8f3a --- /dev/null +++ b/components/admins/components/__tests__/stakingParams.test.tsx @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import React from "react"; +import { screen, cleanup, within } from "@testing-library/react"; +import StakingParams from "@/components/admins/components/stakingParams"; +import matchers from "@testing-library/jest-dom/matchers"; +import {mockStakingParams} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const renderWithProps = (props = {}) => { + const defaultProps = { + stakingParams: mockStakingParams, + isLoading: false, + address: "test_address", + admin: "admin1", + }; + return renderWithChainProvider(); +}; + +describe("StakingParams", () => { + afterEach(cleanup); + + test("renders correctly when not loading", () => { + renderWithProps(); + const stakingParamsContainer = screen.getByLabelText("Staking Params"); + expect(within(stakingParamsContainer).getByText("UNBONDING TIME")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("1")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("MAX VALIDATORS")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("100")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("BOND DENOM")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("upoa")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("MINIMUM COMMISSION")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("5 %")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("MAX ENTRIES")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("7")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("HISTORICAL ENTRIES")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("200")).toBeInTheDocument(); + }); + + test("renders loading state correctly", () => { + renderWithProps({ isLoading: true }); + const stakingParamsContainer = screen.getByLabelText("Skeleton Staking Params"); + expect(within(stakingParamsContainer).getByText("Staking Params")).toBeInTheDocument(); + expect(within(stakingParamsContainer).getByText("Update")).toBeInTheDocument(); + }); + + test("opens update modal on button click", () => { + renderWithProps(); + const stakingParamsContainer = screen.getByLabelText("Skeleton Staking Params"); + const updateButton = within(stakingParamsContainer).getByText("Update"); + updateButton.click(); + const modal = document.getElementById("update-params-modal") as HTMLDialogElement; + expect(modal).toBeInTheDocument(); + expect(modal.open).toBe(true); + }); +}); \ No newline at end of file diff --git a/components/admins/components/stakingParams.tsx b/components/admins/components/stakingParams.tsx index 29132062..54992811 100644 --- a/components/admins/components/stakingParams.tsx +++ b/components/admins/components/stakingParams.tsx @@ -24,7 +24,10 @@ export default function StakingParams({ return (

    -
    +

    Staking Params

    {isLoading &&
    } {!isLoading && ( -
    +
    UNBONDING TIME diff --git a/tests/mock.ts b/tests/mock.ts index 7b6131d6..c2914f04 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -1,5 +1,5 @@ import {Chain} from "@chain-registry/types"; -import {BondStatus} from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; +import {BondStatus, ParamsSDKType} from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; import {ExtendedValidatorSDKType, TransactionGroup} from "@/components"; import {CombinedBalanceInfo} from "@/pages/bank"; @@ -173,3 +173,12 @@ export const mockTransactions: TransactionGroup[] = [ }, ]; +export const mockStakingParams: ParamsSDKType = { + unbonding_time: { seconds: 86400n, nanos: 0 }, + max_validators: 100, + bond_denom: "upoa", + min_commission_rate: "0.05", + max_entries: 7, + historical_entries: 200, +}; + From c0c63bb2698e720982cc58fc9807eacbe83a20f0 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:35:08 -0400 Subject: [PATCH 26/63] test: admin options render --- .../__tests__/adminOptions.test.tsx | 58 +++++++++++++++++++ components/admins/components/adminOptions.tsx | 6 +- tests/mock.ts | 26 +++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 components/admins/components/__tests__/adminOptions.test.tsx diff --git a/components/admins/components/__tests__/adminOptions.test.tsx b/components/admins/components/__tests__/adminOptions.test.tsx new file mode 100644 index 00000000..ea809085 --- /dev/null +++ b/components/admins/components/__tests__/adminOptions.test.tsx @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import React from "react"; +import { screen, cleanup, within } from "@testing-library/react"; +import AdminOptions from "@/components/admins/components/adminOptions"; +import matchers from "@testing-library/jest-dom/matchers"; +import { renderWithChainProvider } from "@/tests/render"; +import { mockPoaParams, mockGroup } from "@/tests/mock"; + +expect.extend(matchers); + +const renderWithProps = (props = {}) => { + const defaultProps = { + poaParams: mockPoaParams, + group: mockGroup, + isLoading: false, + address: "test_address", + admin: "admin1", + }; + return renderWithChainProvider(); +}; + +describe("AdminOptions", () => { + afterEach(cleanup); + + test("renders loading state correctly", () => { + renderWithProps({ isLoading: true }); + expect(screen.getByText("Admin")).toBeInTheDocument(); + }); + + test("renders admin details correctly when not loading", () => { + renderWithProps(); + expect(screen.getByText("Admin")).toBeInTheDocument(); + expect(screen.getByAltText("Profile Avatar")).toBeInTheDocument(); + const titleContainer = screen.getByLabelText("title"); + expect(within(titleContainer).getByText("title1")).toBeInTheDocument(); + const detailsContainer = screen.getByLabelText("details"); + expect(within(detailsContainer).getByText("details1")).toBeInTheDocument(); + }); + + test("opens update modal on button click", () => { + renderWithProps(); + const updateAdminButtonContainer = screen.getByLabelText("update admin"); + const updateButton = within(updateAdminButtonContainer).getByText("Update Admin"); + updateButton.click(); + const modal = document.getElementById("update-admin-modal") as HTMLDialogElement; + expect(modal).toBeInTheDocument(); + expect(modal.open).toBe(true); + }); + + test("opens description modal on button click", () => { + renderWithProps(); + const descriptionButton = screen.getByLabelText("three-dots"); + descriptionButton.click(); + const modal = document.getElementById("description-modal") as HTMLDialogElement; + expect(modal).toBeInTheDocument(); + expect(modal.open).toBe(true); + }); +}); \ No newline at end of file diff --git a/components/admins/components/adminOptions.tsx b/components/admins/components/adminOptions.tsx index ad4930f5..bc287daf 100644 --- a/components/admins/components/adminOptions.tsx +++ b/components/admins/components/adminOptions.tsx @@ -97,16 +97,17 @@ export default function AdminOptions({ size={64} />
    -
    + {group?.ipfsMetadata?.title} - + {group?.ipfsMetadata?.details} @@ -121,6 +122,7 @@ export default function AdminOptions({ diff --git a/tests/mock.ts b/tests/mock.ts index c2914f04..0c001ea6 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -2,6 +2,7 @@ import {Chain} from "@chain-registry/types"; import {BondStatus, ParamsSDKType} from "@chalabi/manifestjs/dist/codegen/cosmos/staking/v1beta1/staking"; import {ExtendedValidatorSDKType, TransactionGroup} from "@/components"; import {CombinedBalanceInfo} from "@/pages/bank"; +import {ExtendedGroupType} from "@/hooks"; export const mockBalances: CombinedBalanceInfo[] = [ { @@ -182,3 +183,28 @@ export const mockStakingParams: ParamsSDKType = { historical_entries: 200, }; +// TODO: Not compatible with alpha.12 as poaParams is not defined in the current version +export const mockPoaParams = { + admins: ["admin1"], + allow_validator_self_exit: true, +}; + +export const mockGroup : ExtendedGroupType = { + id: 1n, + admin: "admin1", + metadata: "metadata1", + version: 1n, + created_at: new Date(), + ipfsMetadata: { + title: "title1", + summary: "summary1", + details: "details1", + authors: "author1", + proposalForumURL: "forum1.com", + voteOptionContext: "context1", + }, + total_weight: "456", + policies: ["policy1"], + members: ["foo", "bar"], +}; + From 99b8eb5b13a495382382357dcb936372e2560bf3 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:16:07 -0400 Subject: [PATCH 27/63] test: denom image --- components/factory/components/DenomImage.tsx | 2 +- .../components/__tests__/DenomImage.test.tsx | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 components/factory/components/__tests__/DenomImage.test.tsx diff --git a/components/factory/components/DenomImage.tsx b/components/factory/components/DenomImage.tsx index d4859808..265ce09b 100644 --- a/components/factory/components/DenomImage.tsx +++ b/components/factory/components/DenomImage.tsx @@ -81,7 +81,7 @@ export const DenomImage = ({ denom }: { denom: any }) => { if (isLoading) { return ( -
    +
    ); } diff --git a/components/factory/components/__tests__/DenomImage.test.tsx b/components/factory/components/__tests__/DenomImage.test.tsx new file mode 100644 index 00000000..39b0bc84 --- /dev/null +++ b/components/factory/components/__tests__/DenomImage.test.tsx @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, test, fireEvent } from "bun:test"; +import React from "react"; +import { screen, cleanup, waitFor } from "@testing-library/react"; +import { DenomImage } from "@/components/factory/components/DenomImage"; +import matchers from "@testing-library/jest-dom/matchers"; +import { renderWithChainProvider } from "@/tests/render"; + +expect.extend(matchers); + +const renderWithProps = (props = {}) => { + const defaultProps = { + denom: { + base: "umfx", + uri: "https://example.com/token.png", + }, + }; + return renderWithChainProvider(); +}; + +describe("DenomImage", () => { + afterEach(cleanup); + + test("renders loading state correctly", () => { + renderWithProps(); + expect(screen.getByLabelText("denom image skeleton")).toBeInTheDocument(); + }); + + // TODO: Make this work with Bun + Next.js Statis Images + // test("renders MFX token image correctly", async () => { + // renderWithProps(); + // await waitFor(() => expect(screen.getByAltText("MFX Token Icon")).toBeInTheDocument()); + // }); + + test("renders ProfileAvatar for unsupported URL", async () => { + renderWithProps({ denom: { base: "unsupported", uri: "https://unsupported.com/token.png" } }); + await waitFor(() => expect(screen.getByAltText("Profile Avatar")).toBeInTheDocument(), { timeout: 2000 }); + }); + + + // TODO: Make this work with Bun + // test("renders image from supported URL", async () => { + // const uri = 'https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHg1aHVqa3NoMG45bzYwbHR5ODk3b2JqbHhnemlmcXpjOXB0enExMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zHcirZSkw8RSDhawFl/giphy.gif'; + // renderWithProps({ denom: { base: "supported", uri: uri } }); + // await waitFor(() => expect(screen.getByAltText("Token Icon")).toBeInTheDocument(), { timeout: 2000 }); + // }); +}); \ No newline at end of file From d1d1e7bd18d7e71e8b9f6d0ce82d5f63ee0aae1a Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:11:54 -0400 Subject: [PATCH 28/63] test: denom info and meta box --- .../__tests__/adminOptions.test.tsx | 8 +-- .../__tests__/stakingParams.test.tsx | 5 +- .../components/__tests__/sendBox.test.tsx | 2 +- components/factory/components/DenomInfo.tsx | 11 +-- components/factory/components/FunctionBox.tsx | 7 -- .../components/__tests__/DenomInfo.test.tsx | 72 +++++++++++++++++++ .../components/__tests__/metaBox.test.tsx | 67 +++++++++++++++++ components/factory/components/index.ts | 1 - components/factory/components/metaBox.tsx | 3 +- tests/mock.ts | 13 ++++ tests/render.tsx | 15 ++-- 11 files changed, 176 insertions(+), 28 deletions(-) delete mode 100644 components/factory/components/FunctionBox.tsx create mode 100644 components/factory/components/__tests__/DenomInfo.test.tsx create mode 100644 components/factory/components/__tests__/metaBox.test.tsx diff --git a/components/admins/components/__tests__/adminOptions.test.tsx b/components/admins/components/__tests__/adminOptions.test.tsx index ea809085..08465d80 100644 --- a/components/admins/components/__tests__/adminOptions.test.tsx +++ b/components/admins/components/__tests__/adminOptions.test.tsx @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; import React from "react"; -import { screen, cleanup, within } from "@testing-library/react"; +import {screen, cleanup, within, fireEvent} from "@testing-library/react"; import AdminOptions from "@/components/admins/components/adminOptions"; import matchers from "@testing-library/jest-dom/matchers"; import { renderWithChainProvider } from "@/tests/render"; @@ -40,8 +40,7 @@ describe("AdminOptions", () => { test("opens update modal on button click", () => { renderWithProps(); const updateAdminButtonContainer = screen.getByLabelText("update admin"); - const updateButton = within(updateAdminButtonContainer).getByText("Update Admin"); - updateButton.click(); + fireEvent.click(within(updateAdminButtonContainer).getByText("Update Admin")); const modal = document.getElementById("update-admin-modal") as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); @@ -49,8 +48,7 @@ describe("AdminOptions", () => { test("opens description modal on button click", () => { renderWithProps(); - const descriptionButton = screen.getByLabelText("three-dots"); - descriptionButton.click(); + fireEvent.click(screen.getByLabelText("three-dots")); const modal = document.getElementById("description-modal") as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); diff --git a/components/admins/components/__tests__/stakingParams.test.tsx b/components/admins/components/__tests__/stakingParams.test.tsx index ae1a8f3a..dd405d89 100644 --- a/components/admins/components/__tests__/stakingParams.test.tsx +++ b/components/admins/components/__tests__/stakingParams.test.tsx @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; import React from "react"; -import { screen, cleanup, within } from "@testing-library/react"; +import {screen, cleanup, within, fireEvent} from "@testing-library/react"; import StakingParams from "@/components/admins/components/stakingParams"; import matchers from "@testing-library/jest-dom/matchers"; import {mockStakingParams} from "@/tests/mock"; @@ -48,8 +48,7 @@ describe("StakingParams", () => { test("opens update modal on button click", () => { renderWithProps(); const stakingParamsContainer = screen.getByLabelText("Skeleton Staking Params"); - const updateButton = within(stakingParamsContainer).getByText("Update"); - updateButton.click(); + fireEvent.click(within(stakingParamsContainer).getByText("Update")); const modal = document.getElementById("update-params-modal") as HTMLDialogElement; expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index f61e0967..52b92331 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -1,7 +1,7 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; -import {fireEvent, screen, cleanup, waitFor} from "@testing-library/react"; +import {screen, cleanup, waitFor, fireEvent} from "@testing-library/react"; import SendBox from "@/components/bank/components/sendBox"; import {mockBalances} from "@/tests/mock"; import {renderWithChainProvider} from "@/tests/render"; diff --git a/components/factory/components/DenomInfo.tsx b/components/factory/components/DenomInfo.tsx index d047ca1e..48fc7b24 100644 --- a/components/factory/components/DenomInfo.tsx +++ b/components/factory/components/DenomInfo.tsx @@ -59,6 +59,7 @@ export default function DenomInfo({ }} className="btn btn-primary btn-xs" disabled={denom?.base.includes("mfx")} + aria-label="update metadata" > Update @@ -76,7 +77,7 @@ export default function DenomInfo({ TICKER - + {denom?.display ?? "No Ticker available"}
    @@ -87,7 +88,9 @@ export default function DenomInfo({ BASE DENOM - +
    + +
    @@ -128,8 +131,8 @@ export default function DenomInfo({ SYMBOL -
    - {denom?.symbol ?? "No Description"} +
    + {denom?.symbol ?? "No Description"}

    {[ ...(denom.base.includes("mfx") ? [] : ["transfer"]), diff --git a/tests/mock.ts b/tests/mock.ts index 0c001ea6..3c5bf922 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -208,3 +208,16 @@ export const mockGroup : ExtendedGroupType = { members: ["foo", "bar"], }; +export const mockDenom = { + base: "TTT", + display: "TEST", + denom_units: [{ denom: "utest1", exponent: 0, aliases: ["utest1"] }, { denom: "test1", exponent: 6, aliases: ["test1"] }], + symbol: "TST", +} + +export const mockMfxDenom = { + base: "umfx", + display: "MFX", + denom_units: [{ denom: "umfx", exponent: 0, aliases: ["umfx"] }, { denom: "mfx", exponent: 6, aliases: ["mfx"] }], + symbol: "umfx" +} \ No newline at end of file diff --git a/tests/render.tsx b/tests/render.tsx index 40025c3e..f6c565e8 100644 --- a/tests/render.tsx +++ b/tests/render.tsx @@ -3,6 +3,7 @@ import {render} from "@testing-library/react"; import {ChainProvider} from "@cosmos-kit/react"; import {ToastProvider} from "@/contexts"; import {defaultAssetLists, defaultChain} from "@/tests/mock" +import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; const defaultOptions = { chains: [defaultChain], @@ -12,11 +13,13 @@ const defaultOptions = { export const renderWithChainProvider = (ui: React.ReactElement, options = {}) => { const combinedOptions = { ...defaultOptions, ...options }; + const client = new QueryClient(); return render( - - - {ui} - - , options); + + + + {ui} + + + , options); }; - From d6a9b4b9907cd0141f380ab05483bf46fba27b16 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:43:12 -0400 Subject: [PATCH 29/63] fix: mock next/image for denom image test --- .../components/__tests__/DenomImage.test.tsx | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/components/factory/components/__tests__/DenomImage.test.tsx b/components/factory/components/__tests__/DenomImage.test.tsx index 39b0bc84..a0cfeaba 100644 --- a/components/factory/components/__tests__/DenomImage.test.tsx +++ b/components/factory/components/__tests__/DenomImage.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test, fireEvent } from "bun:test"; +import { afterEach, describe, expect, test, fireEvent, mock } from "bun:test"; import React from "react"; import { screen, cleanup, waitFor } from "@testing-library/react"; import { DenomImage } from "@/components/factory/components/DenomImage"; @@ -7,11 +7,22 @@ import { renderWithChainProvider } from "@/tests/render"; expect.extend(matchers); +// A cute little candle gif +const uri = 'https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHg1aHVqa3NoMG45bzYwbHR5ODk3b2JqbHhnemlmcXpjOXB0enExMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zHcirZSkw8RSDhawFl/giphy.gif'; + +// Mock next/image +mock.module('next/image', () => ({ + __esModule: true, + default: (props: any) => { + return + }, +})) + const renderWithProps = (props = {}) => { const defaultProps = { denom: { base: "umfx", - uri: "https://example.com/token.png", + uri: uri, }, }; return renderWithChainProvider(); @@ -25,11 +36,10 @@ describe("DenomImage", () => { expect(screen.getByLabelText("denom image skeleton")).toBeInTheDocument(); }); - // TODO: Make this work with Bun + Next.js Statis Images - // test("renders MFX token image correctly", async () => { - // renderWithProps(); - // await waitFor(() => expect(screen.getByAltText("MFX Token Icon")).toBeInTheDocument()); - // }); + test("renders MFX token image correctly", async () => { + renderWithProps(); + await waitFor(() => expect(screen.getByAltText("MFX Token Icon")).toBeInTheDocument(), { timeout: 2000 }); + }); test("renders ProfileAvatar for unsupported URL", async () => { renderWithProps({ denom: { base: "unsupported", uri: "https://unsupported.com/token.png" } }); @@ -37,10 +47,8 @@ describe("DenomImage", () => { }); - // TODO: Make this work with Bun - // test("renders image from supported URL", async () => { - // const uri = 'https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHg1aHVqa3NoMG45bzYwbHR5ODk3b2JqbHhnemlmcXpjOXB0enExMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zHcirZSkw8RSDhawFl/giphy.gif'; - // renderWithProps({ denom: { base: "supported", uri: uri } }); - // await waitFor(() => expect(screen.getByAltText("Token Icon")).toBeInTheDocument(), { timeout: 2000 }); - // }); -}); \ No newline at end of file + test("renders image from supported URL", async () => { + renderWithProps({ denom: { base: "supported", uri: uri } }); + await waitFor(() => expect(screen.getByAltText("Token Icon")).toBeInTheDocument(), { timeout: 2000 }); + }); +}); From efef5a012485e2c67cd3bbfc6805019167acbb7e Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:43:28 -0400 Subject: [PATCH 30/63] test: my denoms --- components/factory/components/MyDenoms.tsx | 2 +- .../components/__tests__/MyDenoms.test.tsx | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 components/factory/components/__tests__/MyDenoms.test.tsx diff --git a/components/factory/components/MyDenoms.tsx b/components/factory/components/MyDenoms.tsx index 841ab401..899705e7 100644 --- a/components/factory/components/MyDenoms.tsx +++ b/components/factory/components/MyDenoms.tsx @@ -60,7 +60,7 @@ export default function MyDenoms({ const renderSkeleton = () => (
    -
    +
    ); diff --git a/components/factory/components/__tests__/MyDenoms.test.tsx b/components/factory/components/__tests__/MyDenoms.test.tsx new file mode 100644 index 00000000..67688f1a --- /dev/null +++ b/components/factory/components/__tests__/MyDenoms.test.tsx @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, test, jest, mock } from "bun:test"; +import React from "react"; +import { screen, cleanup, fireEvent } from "@testing-library/react"; +import MyDenoms from "@/components/factory/components/MyDenoms"; +import matchers from "@testing-library/jest-dom/matchers"; +import { renderWithChainProvider } from "@/tests/render"; +import {mockDenom, mockMfxDenom} from "@/tests/mock"; + +expect.extend(matchers); + +// Mock next/router +const m = jest.fn() +mock.module('next/router', () => ({ + useRouter: m.mockReturnValue({ + query: {}, + push: jest.fn(), + }) +})) + +// TODO: Mock DenomImage until we can fix the URL parsing issue +mock.module('@/components/factory/components/DenomImage', () => ({ + DenomImage: () =>
    DenomImage
    , +})) + +const renderWithProps = (props = {}) => { + const defaultProps = { + denoms: [], + isLoading: false, + isError: null, + refetchDenoms: jest.fn(), + onSelectDenom: jest.fn(), + }; + return renderWithChainProvider(); +}; + +const allDenoms = [mockDenom, mockMfxDenom]; + +describe("MyDenoms", () => { + afterEach(cleanup); + + test("renders loading skeleton when isLoading is true", () => { + renderWithProps({ isLoading: true }); + expect(screen.getByLabelText("skeleton")).toBeInTheDocument(); + }); + + test("renders and selects denoms correctly", () => { + const onSelectDenom = jest.fn(); + renderWithProps({ denoms: allDenoms, onSelectDenom }); + + const denom1 = screen.getByText("TEST"); + fireEvent.click(denom1); + expect(onSelectDenom).toHaveBeenCalledWith(mockDenom); + }); + + test("filters denoms based on search query", () => { + renderWithProps({ denoms: allDenoms }); + + const searchInput = screen.getByPlaceholderText("Search..."); + fireEvent.change(searchInput, { target: { value: "TEST" } }); + expect(screen.getByText("TEST")).toBeInTheDocument(); + expect(screen.queryByText("MFX")).not.toBeInTheDocument(); + }); + + test("displays 'No tokens found' when no denoms match search query", () => { + renderWithProps({ denoms: allDenoms }); + + const searchInput = screen.getByPlaceholderText("Search..."); + fireEvent.change(searchInput, { target: { value: "Nonexistent Denom" } }); + expect(screen.getByText("No tokens found")).toBeInTheDocument(); + }); +}); From 88153992706fd83a6a21e47da392fdc255dae64e Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:48:00 -0400 Subject: [PATCH 31/63] fix: eof eol --- components/admins/components/__tests__/adminOptions.test.tsx | 2 +- components/admins/components/__tests__/stakingParams.test.tsx | 2 +- components/factory/components/__tests__/DenomInfo.test.tsx | 2 +- components/factory/components/__tests__/metaBox.test.tsx | 2 +- components/factory/components/index.ts | 2 +- tests/mock.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/admins/components/__tests__/adminOptions.test.tsx b/components/admins/components/__tests__/adminOptions.test.tsx index 08465d80..d663d461 100644 --- a/components/admins/components/__tests__/adminOptions.test.tsx +++ b/components/admins/components/__tests__/adminOptions.test.tsx @@ -53,4 +53,4 @@ describe("AdminOptions", () => { expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/components/admins/components/__tests__/stakingParams.test.tsx b/components/admins/components/__tests__/stakingParams.test.tsx index dd405d89..a9326898 100644 --- a/components/admins/components/__tests__/stakingParams.test.tsx +++ b/components/admins/components/__tests__/stakingParams.test.tsx @@ -53,4 +53,4 @@ describe("StakingParams", () => { expect(modal).toBeInTheDocument(); expect(modal.open).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/components/factory/components/__tests__/DenomInfo.test.tsx b/components/factory/components/__tests__/DenomInfo.test.tsx index a3b8b9e6..73aa78ce 100644 --- a/components/factory/components/__tests__/DenomInfo.test.tsx +++ b/components/factory/components/__tests__/DenomInfo.test.tsx @@ -69,4 +69,4 @@ describe("DenomInfo", () => { const modal = await waitFor(() => document.getElementById(`denom_info_${mockDenom.base}`)); expect(modal).toBeVisible(); }); -}); \ No newline at end of file +}); diff --git a/components/factory/components/__tests__/metaBox.test.tsx b/components/factory/components/__tests__/metaBox.test.tsx index 1487e68b..932bf1e7 100644 --- a/components/factory/components/__tests__/metaBox.test.tsx +++ b/components/factory/components/__tests__/metaBox.test.tsx @@ -64,4 +64,4 @@ describe("MetaBox", () => { mintTab.click(); await waitFor(() => expect(screen.getByText("Mint TEST")).toBeInTheDocument()); }); -}); \ No newline at end of file +}); diff --git a/components/factory/components/index.ts b/components/factory/components/index.ts index d433b828..a7513748 100644 --- a/components/factory/components/index.ts +++ b/components/factory/components/index.ts @@ -1,4 +1,4 @@ export * from "./DenomInfo"; export * from "./MyDenoms"; export * from './DenomImage' -export * from './metaBox' \ No newline at end of file +export * from './metaBox' diff --git a/tests/mock.ts b/tests/mock.ts index 3c5bf922..eac05d8d 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -220,4 +220,4 @@ export const mockMfxDenom = { display: "MFX", denom_units: [{ denom: "umfx", exponent: 0, aliases: ["umfx"] }, { denom: "mfx", exponent: 6, aliases: ["mfx"] }], symbol: "umfx" -} \ No newline at end of file +} From 1715b036f8e186b5158a165d25ec74b2baa123f5 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:15:03 -0400 Subject: [PATCH 32/63] fix: failing tests --- .../components/__tests__/historyBox.test.tsx | 33 +++++++++------ .../components/__tests__/sendBox.test.tsx | 41 ++++++++++--------- components/bank/components/sendBox.tsx | 5 ++- components/bank/modals/txInfo.tsx | 6 +-- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index 52a567c2..e5a8395f 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -1,7 +1,7 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; -import {render, screen, cleanup} from "@testing-library/react"; +import {render, screen, cleanup, waitFor, fireEvent, within} from "@testing-library/react"; import {HistoryBox} from "@/components/bank/components/historyBox"; import {mockTransactions} from "@/tests/mock"; @@ -42,18 +42,25 @@ describe("HistoryBox", () => { ).toBeInTheDocument(); }); - // TODO: Failing - // test("opens modal when clicking on a transaction", async () => { - // render( - // - // ); - // fireEvent.click(screen.getByText("Send")); - // await waitFor(() => expect(screen.getByRole("dialog")).toBeInTheDocument()); - // }); + test("opens modal when clicking on a transaction", async () => { + render( + + ); + fireEvent.click(screen.getByText("Send")); + await waitFor(() => { + expect(screen.getByLabelText("tx info")).toBeInTheDocument() + expect(screen.getByText("Transaction Details")).toBeInTheDocument(); + + const fromContainer = screen.getByLabelText("from"); + expect(within(fromContainer).getByText("addres...dress1")).toBeInTheDocument(); + const toContainer = screen.getByLabelText("to"); + expect(within(toContainer).getByText("addres...dress2")).toBeInTheDocument(); + }); + }); test("formats date correctly", () => { render( diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index 52b92331..a066fea3 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -1,7 +1,7 @@ import { test, expect, afterEach, describe } from "bun:test"; import React from "react"; import matchers from "@testing-library/jest-dom/matchers"; -import {screen, cleanup, waitFor, fireEvent} from "@testing-library/react"; +import {screen, cleanup, waitFor, fireEvent, within} from "@testing-library/react"; import SendBox from "@/components/bank/components/sendBox"; import {mockBalances} from "@/tests/mock"; import {renderWithChainProvider} from "@/tests/render"; @@ -28,14 +28,18 @@ describe("SendBox", () => { expect(screen.getByText("Send Tokens")).toBeInTheDocument(); }); - // TODO: Failing - // test("toggles between Send and IBC Transfer", () => { - // renderWithProps(); - // expect(screen.getByText("Send Tokens")).toBeInTheDocument(); - // - // fireEvent.click(screen.getByText("IBC Transfer")); - // expect(screen.getByText("IBC Transfer")).toBeInTheDocument(); - // }); + test("toggles between Send and IBC Transfer", async () => { + renderWithProps(); + const buttonContainer = screen.getByLabelText("buttons"); + expect(within(buttonContainer).getByText("Send")).toBeInTheDocument(); + expect(within(buttonContainer).getByText("IBC Transfer")).toBeInTheDocument(); + + const tabsContainer = screen.getByLabelText("tabs"); + expect(within(tabsContainer).getByText("Send Tokens")).toBeInTheDocument(); + + fireEvent.click(within(buttonContainer).getByText("IBC Transfer")); + await waitFor(() => expect(within(tabsContainer).getByText("IBC Transfer")).toBeInTheDocument()); + }); test("displays chain selection dropdown when in IBC Transfer mode", async () => { renderWithProps(); @@ -43,14 +47,13 @@ describe("SendBox", () => { await waitFor(() => expect(screen.getByText("Chain")).toBeInTheDocument()); }); - // TODO: Failing - // test("selects a chain in IBC Transfer mode", async () => { - // renderWithProps(); - // - // fireEvent.click(screen.getByText("IBC Transfer")); - // fireEvent.click(screen.getByText("Chain")); - // fireEvent.click(screen.getByText("Osmosis")); - // - // await waitFor(() => expect(screen.getByText("Osmosis")).toBeInTheDocument()); - // }); + test("selects a chain in IBC Transfer mode", async () => { + renderWithProps(); + const buttonContainer = screen.getByLabelText("buttons"); + expect(within(buttonContainer).getByText("IBC Transfer")).toBeInTheDocument(); + fireEvent.click(within(buttonContainer).getByText("IBC Transfer")); + fireEvent.click(screen.getByText("Chain")); + fireEvent.click(screen.getByLabelText("Osmosis")); + await waitFor(() => expect(screen.getByAltText("Osmosis")).toBeInTheDocument()); + }); }); diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 06d30f4f..137fd5b7 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -31,7 +31,7 @@ export default function SendBox({
    -

    +

    {isIbcTransfer ? "IBC Transfer" : "Send Tokens"}

    @@ -55,6 +55,7 @@ export default function SendBox({ setSelectedChain(chain.id)} className="flex items-center" + aria-label={chain.name} >
    -
    +
    -
    +

    FROM

    @@ -90,7 +90,7 @@ export default function TxInfoModal({ tx, isOpen, onClose }: TxInfoModalProps) {
    -
    +

    TO

    From 28e52403bca8e9044ba9de10886e95f52e43ff83 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:17:21 -0400 Subject: [PATCH 33/63] test: some group component tests --- .../groups/components/CountdownTimer.tsx | 4 + .../__tests__/CountdownTimer.test.tsx | 63 ++++++ .../components/__tests__/groupInfo.test.tsx | 141 +++++++++++++ .../components/__tests__/myGroups.test.tsx | 68 +++++++ components/groups/components/groupInfo.tsx | 52 ++--- components/groups/components/myGroups.tsx | 6 +- .../groups/modals/groupDetailsModal.tsx | 4 +- components/groups/modals/updateGroupModal.tsx | 85 ++++---- hooks/useQueries.ts | 4 +- pages/groups/index.tsx | 2 - tests/mock.ts | 185 +++++++++++++++++- 11 files changed, 534 insertions(+), 80 deletions(-) create mode 100644 components/groups/components/__tests__/CountdownTimer.test.tsx create mode 100644 components/groups/components/__tests__/groupInfo.test.tsx create mode 100644 components/groups/components/__tests__/myGroups.test.tsx diff --git a/components/groups/components/CountdownTimer.tsx b/components/groups/components/CountdownTimer.tsx index 038dfb1e..260b5f20 100644 --- a/components/groups/components/CountdownTimer.tsx +++ b/components/groups/components/CountdownTimer.tsx @@ -33,6 +33,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { days @@ -45,6 +46,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { "--value": timeLeft.hours, } as React.CSSProperties } + aria-label="hours" > hours @@ -57,6 +59,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { "--value": timeLeft.min, } as React.CSSProperties } + aria-label="mins" > min @@ -69,6 +72,7 @@ export default function CountdownTimer({ endTime }: { endTime: Date }) { "--value": timeLeft.sec, } as React.CSSProperties } + aria-label="secs" > sec diff --git a/components/groups/components/__tests__/CountdownTimer.test.tsx b/components/groups/components/__tests__/CountdownTimer.test.tsx new file mode 100644 index 00000000..95788dc1 --- /dev/null +++ b/components/groups/components/__tests__/CountdownTimer.test.tsx @@ -0,0 +1,63 @@ +import { afterEach, describe, expect, test, jest } from "bun:test"; +import React from "react"; +import { screen, cleanup, render } from "@testing-library/react"; +import CountdownTimer from "@/components/groups/components/CountdownTimer"; +import matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); + +describe("CountdownTimer", () => { + afterEach(cleanup); + + test("renders initial state correctly", () => { + jest.useFakeTimers() + jest.setSystemTime(new Date("1992-01-01T00:00:00.000Z")); + const oneSecond = 1000; + const oneMinute = oneSecond * 60; + const oneHour = oneMinute * 60; + const oneDay = oneHour * 24; + + // Now + 2 days - 1 hour - 2 minutes - 1 second + const endTime = new Date(Date.now() + ( 2 * oneDay) - oneHour - (2 * oneMinute) - oneSecond); + render(); + + expect(screen.getByText("days")).toBeInTheDocument(); + const daysSpan = screen.getByLabelText("days"); + expect(daysSpan).toHaveStyle("--value: 1"); + + expect(screen.getByText("hours")).toBeInTheDocument(); + const hoursSpan = screen.getByLabelText("hours"); + expect(hoursSpan).toHaveStyle("--value: 22"); + + expect(screen.getByText("min")).toBeInTheDocument(); + const minSpan = screen.getByLabelText("mins"); + expect(minSpan).toHaveStyle("--value: 57"); + + expect(screen.getByText("sec")).toBeInTheDocument(); + const secSpan = screen.getByLabelText("secs"); + expect(secSpan).toHaveStyle("--value: 59"); + + jest.useRealTimers(); + }); + + test("shows zero values when countdown is complete", () => { + const endTime = new Date(Date.now() - 1000); // 1 second ago + render(); + + expect(screen.getByText("days")).toBeInTheDocument(); + const daysSpan = screen.getByLabelText("days"); + expect(daysSpan).toHaveStyle("--value: 0"); + + expect(screen.getByText("hours")).toBeInTheDocument(); + const hoursSpan = screen.getByLabelText("hours"); + expect(hoursSpan).toHaveStyle("--value: 0"); + + expect(screen.getByText("min")).toBeInTheDocument(); + const minSpan = screen.getByLabelText("mins"); + expect(minSpan).toHaveStyle("--value: 0"); + + expect(screen.getByText("sec")).toBeInTheDocument(); + const secSpan = screen.getByLabelText("secs"); + expect(secSpan).toHaveStyle("--value: 0"); + }); +}); \ No newline at end of file diff --git a/components/groups/components/__tests__/groupInfo.test.tsx b/components/groups/components/__tests__/groupInfo.test.tsx new file mode 100644 index 00000000..0c5528d1 --- /dev/null +++ b/components/groups/components/__tests__/groupInfo.test.tsx @@ -0,0 +1,141 @@ +import { afterEach, describe, expect, test, jest, mock, beforeAll } from "bun:test"; +import React from "react"; +import { screen, cleanup, fireEvent } from "@testing-library/react"; +import {GroupInfo} from "@/components/groups/components/groupInfo"; +import matchers from "@testing-library/jest-dom/matchers"; +import {renderWithChainProvider} from "@/tests/render"; +import {mockGroup} from "@/tests/mock"; + +expect.extend(matchers); + +// Mock the useBalance hook +const m = jest.fn() +mock.module("@/hooks/useQueries", () => ({ + useBalance: m, +})); + +const defaultProps = { + group: mockGroup, + address: "test_address", + policyAddress: "test_policy_address", +}; + + +const renderWithProps = (props = {}) => { + // Not passing + // groupByMemberDataLoading: boolean; + // groupByMemberDataError: boolean | Error | null; + // refetchGroupByMember: () => void; + // is fine as they are not used in the component + return renderWithChainProvider(); +}; + +describe("GroupInfo", () => { + beforeAll(() => { + m.mockReturnValue({ balance: { amount: "1000000" } }); + }); + afterEach(cleanup); + + test("renders initial state correctly", () => { + renderWithProps(); + expect(screen.getByText("Info")).toBeInTheDocument(); + expect(screen.getByText("title1")).toBeInTheDocument(); + expect(screen.getByText("author1")).toBeInTheDocument(); + expect(screen.getByText("author2")).toBeInTheDocument(); + expect(screen.getByText("test_policy_...ddress")).toBeInTheDocument(); + expect(screen.getByText("5 / 10")).toBeInTheDocument(); + }); + + test("renders 'No group Selected' when no group is provided", () => { + renderWithProps({group: null}); + expect(screen.getByText("No group Selected")).toBeInTheDocument(); + }); + + test("renders 'No authors available' when no authors are provided", () => { + const props = { + ...defaultProps, + group: { + ...defaultProps.group, + ipfsMetadata: { + authors: "", + }, + }, + } + renderWithProps({...props}); + expect(screen.getByText("No authors available")).toBeInTheDocument(); + }); + + test("renders 'No balance available' when no balance is provided", () => { + m.mockReturnValue({ balance: { amount: undefined } }); + renderWithProps(); + expect(screen.getByText("No balance available")).toBeInTheDocument(); + }); + + // TODO: The following test fails because we allow the use of the `any` type + // We should properly define all types and avoid using `any` + // test("renders 'No address available' when no policy address is provided", () => { + // const props = { + // ...defaultProps, + // group: { + // ...defaultProps.group, + // policies: [ + // { + // ...defaultProps.group.policies[0], + // address: undefined, + // }, + // ], + // }, + // }; + // renderWithProps({...props}); + // expect(screen.getByText("No address available")).toBeInTheDocument(); + // }); + + test("renders 'No threshold available' when no threshold is provided", () => { + const props = { + ...defaultProps, + group: { + ...defaultProps.group, + policies: [ + { + ...defaultProps.group.policies[0], + decision_policy: { + threshold: undefined, + }, + }, + ], + }, + }; + renderWithProps({...props}); + expect(screen.getByText("No threshold available")).toBeInTheDocument(); + }); + + test("renders 'No total weight available' when no total weight is provided", () => { + const props = { + ...defaultProps, + group: { + ...defaultProps.group, + total_weight: "", + }, + }; + renderWithProps({...props}); + expect(screen.getByText("No total weight available")).toBeInTheDocument(); + }); + + test("triggers update modal on button click", () => { + renderWithProps(); + const updateButton = screen.getByLabelText("update-btn"); + fireEvent.click(updateButton); + const modal = document.getElementById(`update_group_${defaultProps.group.id}`) as HTMLDialogElement; + expect(modal).toBeInTheDocument(); + expect(screen.getByText("Update Group")).toBeInTheDocument(); + }); + + test("triggers group details modal on button click", () => { + renderWithProps(); + const moreInfoButton = screen.getByText("more info"); + fireEvent.click(moreInfoButton); + const modal = document.getElementById(`group_modal_${defaultProps.group.id}`) as HTMLDialogElement; + expect(modal).toBeInTheDocument(); + expect(screen.getByText("Group Details")).toBeInTheDocument(); + }); +}); diff --git a/components/groups/components/__tests__/myGroups.test.tsx b/components/groups/components/__tests__/myGroups.test.tsx new file mode 100644 index 00000000..e51967e3 --- /dev/null +++ b/components/groups/components/__tests__/myGroups.test.tsx @@ -0,0 +1,68 @@ +import { describe, expect, test, jest, mock, afterEach } from "bun:test" +import { screen, cleanup, waitFor, fireEvent } from "@testing-library/react"; +import { YourGroups } from "@/components/groups/components/myGroups"; +import {mockGroup, mockGroup2, mockProposals} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; + +// Mock useRouter +const m = jest.fn() +mock.module('next/router', () => ({ + useRouter: m.mockReturnValue({ + query: {}, + push: jest.fn(), + }) +})) + +const mockOnSelectGroup = jest.fn(); + +function renderWithProps(props = {}) { + const defaultProps = { + groups: { groups: [mockGroup, mockGroup2] }, + groupByMemberDataLoading: false, + groupByMemberDataError: null, + refetchGroupByMember: jest.fn(), + onSelectGroup: mockOnSelectGroup, + proposals: mockProposals, + }; + + return renderWithChainProvider(); +} + +describe("YourGroups Component", () => { + afterEach(cleanup) + + test("renders empty group state correctly", () => { + renderWithProps({groups: { groups: []}}); + expect(screen.getByText("My Groups")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); + expect(screen.getByText("No groups found")).toBeInTheDocument(); + }); + + test("renders loading state correctly", () => { + renderWithProps(); + expect(screen.getByText("My Groups")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); + expect(screen.getByText("title1")).toBeInTheDocument(); + expect(screen.getByText("title2")).toBeInTheDocument(); + }); + + + test("search functionality works correctly", () => { + renderWithProps(); + + const searchInput = screen.getByPlaceholderText("Search..."); + fireEvent.change(searchInput, { target: { value: "title1" } }); + + expect(screen.getByText("title1")).toBeInTheDocument(); + expect(screen.queryByText("title2")).not.toBeInTheDocument(); + }); + + test("group selection works correctly", async () => { + mockOnSelectGroup.mockClear() + + renderWithProps(); + const group1 = screen.getByText("title1"); + fireEvent.click(group1); + await waitFor(() => expect(mockOnSelectGroup).toHaveBeenLastCalledWith("test_policy_address")); + }); +}); diff --git a/components/groups/components/groupInfo.tsx b/components/groups/components/groupInfo.tsx index 569e741f..9e94bf59 100644 --- a/components/groups/components/groupInfo.tsx +++ b/components/groups/components/groupInfo.tsx @@ -1,45 +1,46 @@ import { - ExtendedQueryGroupsByMemberResponseSDKType, useBalance, } from "@/hooks/useQueries"; -import { GroupDetailsModal } from "../modals/groupDetailsModal"; +import { GroupDetailsModal, UpdateGroupModal } from "@/components"; import { TruncatedAddressWithCopy } from "@/components/react/addressCopy"; -import Link from "next/link"; -import { shiftDigits, truncateString } from "@/utils"; -import { Key, useEffect, useState } from "react"; +import { shiftDigits } from "@/utils"; +import { Key } from "react"; import { PiArrowUpRightLight } from "react-icons/pi"; -import { UpdateGroupModal } from "../modals/updateGroupModal"; export function GroupInfo({ group, - groupByMemberDataLoading, - groupByMemberDataError, - refetchGroupByMember, address, policyAddress, -}: { - group: any; +}: Readonly<{ + group: any; // TODO: Define type groupByMemberDataLoading: boolean; groupByMemberDataError: Error | null | boolean; refetchGroupByMember: () => void; address: string; policyAddress: string; -}) { - const { balance } = useBalance(group?.policies?.[0]?.address); +}>) { + // TODO: The policy address is passed to this component but we still use `group.policies?.[0]?.address` to get the policy address + + const maybeAuthors = group?.ipfsMetadata?.authors; + const maybePolicies = group?.policies?.[0]; + + const threshold = maybePolicies?.decision_policy?.threshold ?? "No threshold available"; + + const { balance } = useBalance(maybePolicies?.address); const renderAuthors = () => { - if (group?.ipfsMetadata?.authors) { - if (group.ipfsMetadata.authors.startsWith("manifest")) { + if (maybeAuthors) { + if (maybeAuthors.startsWith("manifest")) { return ( ); - } else if (group.ipfsMetadata.authors.includes(",")) { + } else if (maybeAuthors.includes(",")) { return (
    - {group.ipfsMetadata.authors + {maybeAuthors .split(",") .map((author: string, index: Key | null | undefined) => (
    @@ -56,7 +57,7 @@ export function GroupInfo({
    ); } else { - return {group.ipfsMetadata.authors}; + return {maybeAuthors}; } } else { return No authors available; @@ -77,6 +78,7 @@ export function GroupInfo({ modal?.showModal(); }} className="btn-xs btn btn-primary " + aria-label="update-btn" > Update @@ -132,14 +134,14 @@ export function GroupInfo({ POLICY ADDRESS -

    + -

    +
    @@ -147,9 +149,11 @@ export function GroupInfo({
    - {group?.policies?.[0]?.decision_policy?.threshold ?? - "No threshold available"}{" "} - / {group?.total_weight ?? "No total weight available"} + {maybePolicies.decision_policy?.threshold && group?.total_weight + ? `${maybePolicies.decision_policy.threshold} / ${group.total_weight}` + : maybePolicies.decision_policy?.threshold + ? "No total weight available" + : "No threshold available"}
    diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 5dbfa862..e41d3c9c 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -12,14 +12,14 @@ export function YourGroups({ refetchGroupByMember, onSelectGroup, proposals, -}: { +}: Readonly<{ groups: ExtendedQueryGroupsByMemberResponseSDKType; groupByMemberDataLoading: boolean; groupByMemberDataError: Error | null | boolean; refetchGroupByMember: () => void; onSelectGroup: (policyAddress: string) => void; - proposals: any; -}) { + proposals: any; // TODO: Define type +}>) { const [selectedGroup, setSelectedGroup] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const router = useRouter(); diff --git a/components/groups/modals/groupDetailsModal.tsx b/components/groups/modals/groupDetailsModal.tsx index 5da58c7f..5391626b 100644 --- a/components/groups/modals/groupDetailsModal.tsx +++ b/components/groups/modals/groupDetailsModal.tsx @@ -133,13 +133,11 @@ export function GroupDetailsModal({

    ADMIN

    -
    -

    +

    {" "} -

    diff --git a/components/groups/modals/updateGroupModal.tsx b/components/groups/modals/updateGroupModal.tsx index 02968ebe..4d76b10e 100644 --- a/components/groups/modals/updateGroupModal.tsx +++ b/components/groups/modals/updateGroupModal.tsx @@ -23,7 +23,7 @@ interface Group { metadata: string; ipfsMetadata: IPFSMetadata | null; members: { group_id: string; member: Member }[]; - policies: any[]; + policies: any[]; // TODO: Define type }; address: string; } @@ -37,6 +37,18 @@ export function UpdateGroupModal({ const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); + const maybeIpfsMetadata = group?.ipfsMetadata; + const maybeTitle = maybeIpfsMetadata?.title; + const maybeAuthors = maybeIpfsMetadata?.authors; + const maybeSummary = maybeIpfsMetadata?.summary; + const maybeProposalForumURL = maybeIpfsMetadata?.proposalForumURL; + const maybeDetails = maybeIpfsMetadata?.details; + const maybePolicies = group?.policies?.[0]; + const maybeDecisionPolicy = maybePolicies?.decision_policy; + const maybeThreshold = maybeDecisionPolicy?.threshold; + const maybeVotingPeriod = maybeDecisionPolicy?.windows?.voting_period; + const maybeMembers = group?.members; + const { updateGroupAdmin, updateGroupMembers, @@ -46,29 +58,22 @@ export function UpdateGroupModal({ updateGroupPolicyMetadata, } = cosmos.group.v1.MessageComposer.withTypeUrl; - const [name, setName] = useState(group.ipfsMetadata?.title || ""); - const [authors, setAuthors] = useState(group.ipfsMetadata?.authors || ""); - const [summary, setSummary] = useState(group.ipfsMetadata?.summary || ""); - const [forum, setForum] = useState( - group.ipfsMetadata?.proposalForumURL || "" - ); - const [description, setDescription] = useState( - group.ipfsMetadata?.details || "" - ); - const [threshold, setThreshold] = useState( - group.policies[0]?.decision_policy?.threshold || "" - ); + const [name, setName] = useState(maybeTitle ?? ""); + const [authors, setAuthors] = useState(maybeAuthors ?? ""); + const [summary, setSummary] = useState(maybeSummary ?? ""); + const [forum, setForum] = useState(maybeProposalForumURL ?? ""); + const [description, setDescription] = useState(maybeDetails ?? ""); + const [threshold, setThreshold] = useState(maybeThreshold ?? ""); const [windowInput, setWindowInput] = useState(""); const [votingUnit, setVotingUnit] = useState("days"); const [isSigning, setIsSigning] = useState(false); useEffect(() => { - setName(group.ipfsMetadata?.title || ""); - setAuthors(group.ipfsMetadata?.authors || ""); - setSummary(group.ipfsMetadata?.summary || ""); - setForum(group.ipfsMetadata?.proposalForumURL || ""); - setDescription(group.ipfsMetadata?.details || ""); - setThreshold(group.policies[0]?.decision_policy?.threshold || ""); + setAuthors(maybeAuthors ?? ""); + setSummary(maybeSummary ?? ""); + setForum(maybeProposalForumURL ?? ""); + setDescription(maybeDetails ?? ""); + setThreshold(maybeThreshold ?? ""); }, [group]); const convertToSeconds = (input: string, unit: string) => { @@ -110,7 +115,7 @@ export function UpdateGroupModal({ }; const votingWindow = parseFloat( - group?.policies[0]?.decision_policy?.windows?.voting_period.slice(0, -1) + maybeVotingPeriod?.slice(0, -1) ); let formattedVotingWindow; @@ -137,12 +142,12 @@ export function UpdateGroupModal({ }; const isPolicyAdmin = (address: string) => { - const adminAddresses = [group.policies[0]?.admin].filter(Boolean); + const adminAddresses = [maybePolicies?.admin].filter(Boolean); return adminAddresses.includes(address); }; const initializeMembers = () => { - return group.members.map((member) => ({ + return maybeMembers?.map((member) => ({ group_id: member.group_id, member: member.member, isCoreMember: true, @@ -216,7 +221,7 @@ export function UpdateGroupModal({ if (hasStateChanged(newAdmin, group.admin)) { const msg = updateGroupAdmin({ admin: group.admin, - groupId: BigInt(group?.members[0]?.group_id), + groupId: BigInt(maybeMembers?.[0]?.group_id), newAdmin: newAdmin ?? "", }); messages.push( @@ -246,7 +251,7 @@ export function UpdateGroupModal({ if (membersChanged) { const msg = updateGroupMembers({ admin: group.admin, - groupId: BigInt(group?.members[0]?.group_id), + groupId: BigInt(maybeMembers?.[0]?.group_id), memberUpdates: members.map((member) => ({ address: member.member.address, metadata: member.member.metadata, @@ -265,11 +270,11 @@ export function UpdateGroupModal({ // Update Group Metadata if ( - hasStateChanged(name, group.ipfsMetadata?.title) || - hasStateChanged(authors, group.ipfsMetadata?.authors) || - hasStateChanged(summary, group.ipfsMetadata?.summary) || - hasStateChanged(forum, group.ipfsMetadata?.proposalForumURL) || - hasStateChanged(description, group.ipfsMetadata?.details) + hasStateChanged(name, maybeTitle) || + hasStateChanged(authors, maybeAuthors) || + hasStateChanged(summary, maybeSummary) || + hasStateChanged(forum, maybeProposalForumURL) || + hasStateChanged(description, maybeDetails) ) { const newMetadata = JSON.stringify({ title: name, @@ -280,7 +285,7 @@ export function UpdateGroupModal({ }); const msgGroupMetadata = updateGroupMetadata({ admin: group.admin, - groupId: BigInt(group?.members[0]?.group_id), + groupId: BigInt(maybeMembers?.[0]?.group_id), metadata: newMetadata, }); messages.push( @@ -293,7 +298,7 @@ export function UpdateGroupModal({ ); const msgPolicyMetadata = updateGroupPolicyMetadata({ - groupPolicyAddress: group.policies[0].address, + groupPolicyAddress: maybePolicies?.address, admin: group.admin, metadata: newMetadata, }); @@ -310,9 +315,9 @@ export function UpdateGroupModal({ // Update Group Policy Admin const newPolicyAdmin = members?.find((member) => member?.isPolicyAdmin) ?.member?.address; - if (hasStateChanged(newPolicyAdmin, group.policies[0]?.admin)) { + if (hasStateChanged(newPolicyAdmin, maybePolicies?.admin)) { const msg = updateGroupPolicyAdmin({ - groupPolicyAddress: group.policies[0].address, + groupPolicyAddress: maybePolicies?.address, admin: group.admin, newAdmin: newPolicyAdmin ?? "", }); @@ -330,11 +335,11 @@ export function UpdateGroupModal({ if ( hasStateChanged( threshold, - group.policies[0]?.decision_policy?.threshold + maybeThreshold ) || hasStateChanged( windowSeconds, - group.policies[0]?.decision_policy?.windows?.voting_period.seconds + maybeVotingPeriod?.seconds ) ) { const thresholdMsg = { @@ -352,7 +357,7 @@ export function UpdateGroupModal({ ).finish(); const msg = updateGroupPolicyDecisionPolicy({ - groupPolicyAddress: group.policies[0].address, + groupPolicyAddress: maybePolicies?.address, admin: group.admin, decisionPolicy: { threshold: threshold, @@ -487,9 +492,7 @@ export function UpdateGroupModal({ value={threshold} onChange={(e) => setThreshold(e.target.value)} className="input input-bordered w-full" - placeholder={ - group?.policies[0]?.decision_policy?.threshold ?? - "No threshold available" + placeholder={maybeThreshold ?? "No threshold available" } />
    @@ -507,9 +510,7 @@ export function UpdateGroupModal({ value={forum} onChange={(e) => setForum(e.target.value)} className="input input-bordered w-full" - placeholder={ - group?.ipfsMetadata?.proposalForumURL ?? - "No forum URL available" + placeholder={maybeProposalForumURL ?? "No forum URL available" } />
    diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index a5027503..09c7797c 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -20,8 +20,8 @@ export interface IPFSMetadata { export type ExtendedGroupType = QueryGroupsByMemberResponseSDKType['groups'][0] & { ipfsMetadata: IPFSMetadata | null; - policies: any[]; - members: any[]; + policies: any[]; // TODO: Define type + members: any[]; // TODO: Define type }; export interface ExtendedQueryGroupsByMemberResponseSDKType { diff --git a/pages/groups/index.tsx b/pages/groups/index.tsx index cf439289..643af5ec 100644 --- a/pages/groups/index.tsx +++ b/pages/groups/index.tsx @@ -61,7 +61,6 @@ export default function Groups() { // await channel.publish("new-proposal", proposal); // }; return ( - <>
    {/*
    - ); } diff --git a/tests/mock.ts b/tests/mock.ts index eac05d8d..2ad40898 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -3,6 +3,11 @@ import {BondStatus, ParamsSDKType} from "@chalabi/manifestjs/dist/codegen/cosmos import {ExtendedValidatorSDKType, TransactionGroup} from "@/components"; import {CombinedBalanceInfo} from "@/pages/bank"; import {ExtendedGroupType} from "@/hooks"; +import { + ProposalExecutorResult, + ProposalSDKType, + ProposalStatus +} from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; export const mockBalances: CombinedBalanceInfo[] = [ { @@ -199,13 +204,92 @@ export const mockGroup : ExtendedGroupType = { title: "title1", summary: "summary1", details: "details1", - authors: "author1", + authors: "author1, author2", proposalForumURL: "forum1.com", voteOptionContext: "context1", }, - total_weight: "456", - policies: ["policy1"], - members: ["foo", "bar"], + total_weight: "10", + policies: [ + { + address: "test_policy_address", + decision_policy: { + threshold: "5", + }, + }, + ], + members: [ + { + group_id: 1, + member: { + address: "test_address1", + weight: "5", + metadata: "test_metadata1", + added_at: Date.now(), + isCoreMember: true, + isActive: true, + }, + }, + { + group_id: 1, + member: { + address: "test_address2", + weight: "5", + metadata: "test_metadata2", + added_at: Date.now(), + isCoreMember: true, + isActive: true, + }, + } + ], +}; + +export const mockGroup2 : ExtendedGroupType = { + id: 2n, + admin: "admin2", + metadata: "metadata2", + version: 1n, + created_at: new Date(), + ipfsMetadata: { + title: "title2", + summary: "summary2", + details: "details2", + authors: "author2, author3", + proposalForumURL: "forum2.com", + voteOptionContext: "context2", + }, + total_weight: "10", + policies: [ + { + address: "test_policy_address2", + decision_policy: { + threshold: "5", + }, + }, + ], + members: [ + { + group_id: 2, + member: { + address: "test_address2", + weight: "5", + metadata: "test_metadata2", + added_at: Date.now(), + isCoreMember: true, + isActive: true, + }, + }, + { + group_id: 2, + member: { + address: "test_address3", + weight: "5", + metadata: "test_metadata3", + added_at: Date.now(), + isCoreMember: true, + isActive: true, + }, + } + ], }; export const mockDenom = { @@ -221,3 +305,96 @@ export const mockMfxDenom = { denom_units: [{ denom: "umfx", exponent: 0, aliases: ["umfx"] }, { denom: "mfx", exponent: 6, aliases: ["mfx"] }], symbol: "umfx" } + +export const mockProposals: { [key: string]: ProposalSDKType[] } = { + // The key should match the policy address from `mockGroup` + test_policy_address: [ + { + id: 1n, + title: "title1", + group_policy_address: "policy1", + summary: "summary1", + metadata: "metadata1", + proposers: ["proposer1"], + submit_time: new Date(), + group_version: 1n, + group_policy_version: 1n, + status: ProposalStatus.PROPOSAL_STATUS_SUBMITTED, + final_tally_result: { + yes_count: "1", + abstain_count: "0", + no_count: "0", + no_with_veto_count: "0", + }, + voting_period_end: new Date(), + executor_result: ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + messages: [], + }, + { + id: 2n, + title: "title2", + group_policy_address: "policy2", + summary: "summary2", + metadata: "metadata2", + proposers: ["proposer2"], + submit_time: new Date(), + group_version: 1n, + group_policy_version: 1n, + status: ProposalStatus.PROPOSAL_STATUS_ACCEPTED, + final_tally_result: { + yes_count: "1", + abstain_count: "0", + no_count: "0", + no_with_veto_count: "0", + }, + voting_period_end: new Date(), + executor_result: ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + messages: [], + }, + ], + // The key should match the policy address from `mockGroup2` + test_policy_address2: [ + { + id: 3n, + title: "title3", + group_policy_address: "policy3", + summary: "summary3", + metadata: "metadata3", + proposers: ["proposer3"], + submit_time: new Date(), + group_version: 1n, + group_policy_version: 1n, + status: ProposalStatus.PROPOSAL_STATUS_REJECTED, + final_tally_result: { + yes_count: "0", + abstain_count: "0", + no_count: "1", + no_with_veto_count: "0", + }, + voting_period_end: new Date(), + executor_result: ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE, + messages: [], + }, + { + id: 4n, + title: "title4", + group_policy_address: "policy4", + summary: "summary4", + metadata: "metadata4", + proposers: ["proposer4"], + submit_time: new Date(), + group_version: 1n, + group_policy_version: 1n, + status: ProposalStatus.PROPOSAL_STATUS_WITHDRAWN, + final_tally_result: { + yes_count: "1", + abstain_count: "0", + no_count: "0", + no_with_veto_count: "0", + }, + voting_period_end: new Date(), + executor_result: ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + messages: [], + } + ] +}; From 7ad61f7ce4e2c265eff33f73a6261d6e96aa46c8 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:20:53 -0400 Subject: [PATCH 34/63] fix: eof eol --- components/groups/components/__tests__/CountdownTimer.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/groups/components/__tests__/CountdownTimer.test.tsx b/components/groups/components/__tests__/CountdownTimer.test.tsx index 95788dc1..9e659fa3 100644 --- a/components/groups/components/__tests__/CountdownTimer.test.tsx +++ b/components/groups/components/__tests__/CountdownTimer.test.tsx @@ -60,4 +60,4 @@ describe("CountdownTimer", () => { const secSpan = screen.getByLabelText("secs"); expect(secSpan).toHaveStyle("--value: 0"); }); -}); \ No newline at end of file +}); From 18af395753dddd7277277f24577f7570f6ed04f3 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:11:39 -0400 Subject: [PATCH 35/63] test: step indicator --- .../groups/components/StepIndicator.tsx | 4 +- .../__tests__/StepIndicator.test.tsx | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 components/groups/components/__tests__/StepIndicator.test.tsx diff --git a/components/groups/components/StepIndicator.tsx b/components/groups/components/StepIndicator.tsx index 5ff337d5..97241bc9 100644 --- a/components/groups/components/StepIndicator.tsx +++ b/components/groups/components/StepIndicator.tsx @@ -3,10 +3,10 @@ import React, { ReactNode } from "react"; export default function StepIndicator({ currentStep, steps, -}: { +}: Readonly<{ currentStep: number; steps: { label: ReactNode; step: number }[]; -}) { +}>) { return (
      {steps.map(({ label, step }) => ( diff --git a/components/groups/components/__tests__/StepIndicator.test.tsx b/components/groups/components/__tests__/StepIndicator.test.tsx new file mode 100644 index 00000000..3f88dc65 --- /dev/null +++ b/components/groups/components/__tests__/StepIndicator.test.tsx @@ -0,0 +1,39 @@ +import {describe, test, expect, afterEach} from "bun:test" +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import StepIndicator from '@/components/groups/components/StepIndicator'; + +describe('StepIndicator Component', () => { + afterEach(cleanup) + + const steps = [ + { label: 'Step 1', step: 1 }, + { label: 'Step 2', step: 2 }, + { label: 'Step 3', step: 3 }, + ]; + + test('renders steps correctly', () => { + render(); + expect(screen.getByText('Step 1')).toBeInTheDocument(); + expect(screen.getByText('Step 2')).toBeInTheDocument(); + expect(screen.getByText('Step 3')).toBeInTheDocument(); + }); + + test('highlights the current step correctly', () => { + render(); + const currentStep = screen.getByText('Step 2'); + expect(currentStep).toHaveClass('step-primary'); + }); + + test('highlights the steps before the current step correctly', () => { + render(); + const previousStep = screen.getByText('Step 1'); + expect(previousStep).toHaveClass('step-primary'); + }); + + test('does not highlight the steps after the current step', () => { + render(); + const nextStep = screen.getByText('Step 3'); + expect(nextStep).not.toHaveClass('step-primary'); + }); +}); \ No newline at end of file From f3a38b3dd6b51c86796e8fd44bac92c6dc41ae05 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:26:42 -0400 Subject: [PATCH 36/63] test: partial admin modals tests --- .../__tests__/descriptionModal.test.tsx | 36 +++++++++ .../__tests__/updateAdminModal.test.tsx | 63 +++++++++++++++ .../updateStakingParamsModal.test.tsx | 77 +++++++++++++++++++ components/admins/modals/descriptionModal.tsx | 4 +- components/admins/modals/updateAdminModal.tsx | 2 +- .../modals/updateStakingParamsModal.tsx | 19 +++-- .../__tests__/StepIndicator.test.tsx | 3 + 7 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 components/admins/modals/__tests__/descriptionModal.test.tsx create mode 100644 components/admins/modals/__tests__/updateAdminModal.test.tsx create mode 100644 components/admins/modals/__tests__/updateStakingParamsModal.test.tsx diff --git a/components/admins/modals/__tests__/descriptionModal.test.tsx b/components/admins/modals/__tests__/descriptionModal.test.tsx new file mode 100644 index 00000000..1e1f987f --- /dev/null +++ b/components/admins/modals/__tests__/descriptionModal.test.tsx @@ -0,0 +1,36 @@ +import {describe, test, afterEach, expect} from "bun:test"; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { DescriptionModal } from '@/components/admins/modals/descriptionModal'; +import matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); + +describe('DescriptionModal Component', () => { + const modalId = 'test-modal'; + const details = 'This is a test description.'; + + afterEach(cleanup); + + test('renders modal with correct details', () => { + render(); + expect(screen.getByText('Group Description')).toBeInTheDocument(); + expect(screen.getByText(details)).toBeInTheDocument(); + }); + + test('displays correct title for validator type', () => { + render(); + expect(screen.getByText('Validator Description')).toBeInTheDocument(); + }); + + // TODO: Why is this test failing? + // // https://github.com/capricorn86/happy-dom/issues/1184 + // test('closes modal when close button is clicked', async () => { + // render(); + // expect(screen.getByText(details)).toBeInTheDocument(); + // expect(screen.getByLabelText('x-close')).toBeInTheDocument(); + // const closeButton = screen.getByLabelText('x-close'); + // fireEvent.click(closeButton); + // await waitFor(() => expect(screen.queryByText(details)).not.toBeInTheDocument()); + // }); +}); \ No newline at end of file diff --git a/components/admins/modals/__tests__/updateAdminModal.test.tsx b/components/admins/modals/__tests__/updateAdminModal.test.tsx new file mode 100644 index 00000000..ad6439e5 --- /dev/null +++ b/components/admins/modals/__tests__/updateAdminModal.test.tsx @@ -0,0 +1,63 @@ +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { UpdateAdminModal } from '@/components/admins/modals/updateAdminModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const modalId = 'test-modal'; +const admin = 'manifest1adminaddress'; +const userAddress = 'manifest1useraddress'; +const validAddress = 'manifest1hj5fveer5cjtn4wd6wstzugjfdxzl0xp8ws9ct'; +const allowExit = true; + +function renderWithProps(props = {}) { + return renderWithChainProvider(); +} + +describe('UpdateAdminModal Component', () => { + afterEach(cleanup); + + test('renders modal with correct details', () => { + renderWithProps(); + expect(screen.getByText('Update Admin')).toBeInTheDocument(); + expect(screen.getByText('Warning')).toBeInTheDocument(); + expect(screen.getByText('Currently, the admin is set to a group policy address. While the admin can be any manifest1 address, it is recommended to set the new admin to another group policy address.')).toBeInTheDocument(); + }); + + test('updates input field correctly', () => { + renderWithProps(); + const input = screen.getByPlaceholderText('manifest123...'); + fireEvent.change(input, { target: { value: 'manifest1newadminaddress' } }); + expect(input).toHaveValue('manifest1newadminaddress'); + }); + + test('disables update button when input is invalid', () => { + renderWithProps(); + const input = screen.getByPlaceholderText('manifest123...'); + const updateButton = screen.getByText('Update'); + expect(updateButton).toBeDisabled(); + fireEvent.change(input, { target: { value: 'invalidaddress' } }); + expect(updateButton).toBeDisabled(); + }); + + test('enables update button when input is valid', () => { + renderWithProps(); + const updateButton = screen.getByText('Update'); + expect(updateButton).toBeDisabled(); + const input = screen.getByPlaceholderText('manifest123...'); + fireEvent.change(input, { target: { value: validAddress } }); + expect(updateButton).toBeEnabled(); + }); + + // // TODO: Why is this test failing? + // // https://github.com/capricorn86/haVyppy-dom/issues/1184 + // test('closes modal when close button is clicked', async () => { + // renderWithProps(); + // const closeButton = screen.getByText('✕'); + // fireEvent.click(closeButton); + // await waitFor(() => expect(screen.queryByText('Update Admin')).not.toBeInTheDocument()); + // }); +}); \ No newline at end of file diff --git a/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx new file mode 100644 index 00000000..d1c31ea7 --- /dev/null +++ b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx @@ -0,0 +1,77 @@ +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { UpdateStakingParamsModal } from '@/components/admins/modals/updateStakingParamsModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import {renderWithChainProvider} from "@/tests/render"; +import {DurationSDKType} from "@chalabi/manifestjs/src/codegen/google/protobuf/duration"; + +expect.extend(matchers); + +const modalId = 'test-modal'; +const stakingParams:{ + unbonding_time: DurationSDKType, + max_validators: number, + bond_denom: string, + min_commission_rate: string, + max_entries: number, + historical_entries: number, +} = { + unbonding_time: { seconds: BigInt(86400), nanos: 0 }, + max_validators: 100, + bond_denom: 'stake', + min_commission_rate: '0.05', + max_entries: 7, + historical_entries: 100, +}; +const admin = 'manifest1adminaddress'; +const address = 'manifest1useraddress'; + +function renderWithProps(props = {}) { + renderWithChainProvider(); +} + +describe('UpdateStakingParamsModal Component', () => { + afterEach(cleanup); + + test('renders modal with correct details', () => { + renderWithProps(); + expect(screen.getByText('Update Staking Parameters')).toBeInTheDocument(); + expect(screen.getByText('UNBONDING TIME')).toBeInTheDocument(); + expect(screen.getByText('MAX VALIDATORS')).toBeInTheDocument(); + expect(screen.getByText('BOND DENOM')).toBeInTheDocument(); + expect(screen.getByText('MINIMUM COMMISSION')).toBeInTheDocument(); + expect(screen.getByText('MAX ENTRIES')).toBeInTheDocument(); + expect(screen.getByText('HISTORICAL ENTRIES')).toBeInTheDocument(); + }); + + test('updates input fields correctly', () => { + renderWithProps(); + const unbondingTimeInput = screen.getByPlaceholderText('1'); + fireEvent.change(unbondingTimeInput, { target: { value: 2 } }); + expect(unbondingTimeInput).toHaveValue(2); + }); + + test('disables update button when no changes are made', () => { + renderWithProps(); + const updateButton = screen.getByText('Update'); + expect(updateButton).toBeDisabled(); + }); + + test('enables update button when changes are made', () => { + renderWithProps(); + const unbondingTimeInput = screen.getByPlaceholderText('1'); + fireEvent.change(unbondingTimeInput, { target: { value: 2 } }); + const updateButton = screen.getByText('Update'); + expect(updateButton).toBeEnabled(); + }); + + // // TODO: Why is this test failing? + // // https://github.com/capricorn86/happy-dom/issues/1184 + // test('closes modal when close button is clicked', async () => { + // renderWithProps(); + // const closeButton = screen.getByText('✕'); + // fireEvent.click(closeButton); + // await waitFor(() => expect(screen.queryByText('Update Staking Parameters')).not.toBeInTheDocument()); + // }); +}); \ No newline at end of file diff --git a/components/admins/modals/descriptionModal.tsx b/components/admins/modals/descriptionModal.tsx index 2dbc32e1..906665de 100644 --- a/components/admins/modals/descriptionModal.tsx +++ b/components/admins/modals/descriptionModal.tsx @@ -11,11 +11,11 @@ export function DescriptionModal({ modalId, details, type, -}: DescriptionModalProps) { +}: Readonly) { return ( -

      diff --git a/components/admins/modals/updateAdminModal.tsx b/components/admins/modals/updateAdminModal.tsx index 27f5be84..bc0807dd 100644 --- a/components/admins/modals/updateAdminModal.tsx +++ b/components/admins/modals/updateAdminModal.tsx @@ -18,7 +18,7 @@ export function UpdateAdminModal({ admin, userAddress, allowExit, -}: UpdateModalProps) { +}: Readonly) { const [newAdmin, setNewAdmin] = useState(""); const [isValidAddress, setIsValidAddress] = useState(false); diff --git a/components/admins/modals/updateStakingParamsModal.tsx b/components/admins/modals/updateStakingParamsModal.tsx index 1861cc36..20e06e6b 100644 --- a/components/admins/modals/updateStakingParamsModal.tsx +++ b/components/admins/modals/updateStakingParamsModal.tsx @@ -18,7 +18,7 @@ export function UpdateStakingParamsModal({ stakingParams, admin, address, -}: UpdateStakingParamsModalProps) { +}: Readonly) { const [unbondingTime, setUnbondingTime] = useState(""); const [maxValidators, setMaxValidators] = useState(""); const [bondDenom, setBondDenom] = useState(""); @@ -103,7 +103,8 @@ export function UpdateStakingParamsModal({ value: string, setter: React.Dispatch>, tip: string, - type: string = "text" + placeholder: string, + type: string = "text", ) => (
      {label} @@ -112,9 +113,7 @@ export function UpdateStakingParamsModal({ type={type} value={value} onChange={(e) => setter(e.target.value)} - placeholder={ - (stakingParams as any)[label.toLowerCase().replace(/\s/g, "_")] || "" - } + placeholder={placeholder} /> {tip}
      @@ -136,6 +135,7 @@ export function UpdateStakingParamsModal({ unbondingTime, setUnbondingTime, "Enter time in days", + "1", "number" )} {renderInput( @@ -143,6 +143,7 @@ export function UpdateStakingParamsModal({ maxValidators, setMaxValidators, "Maximum number of validators", + stakingParams.max_validators.toString(), "number" )}

    @@ -151,13 +152,15 @@ export function UpdateStakingParamsModal({ "BOND DENOM", bondDenom, setBondDenom, - "Token denomination for bonding" + "Token denomination for bonding", + stakingParams.bond_denom )} {renderInput( "MINIMUM COMMISSION", minCommissionRate, setMinCommissionRate, - "Commission rate (e.g., 0.05 for 5%)" + "Commission rate (e.g., 0.05 for 5%)", + stakingParams.min_commission_rate, )}
    @@ -166,6 +169,7 @@ export function UpdateStakingParamsModal({ maxEntries, setMaxEntries, "Maximum entries for either unbonding delegation or redelegation", + stakingParams.max_entries.toString(), "number" )} {renderInput( @@ -173,6 +177,7 @@ export function UpdateStakingParamsModal({ historicalEntries, setHistoricalEntries, "Number of historical entries to persist", + stakingParams.historical_entries.toString(), "number" )}
    diff --git a/components/groups/components/__tests__/StepIndicator.test.tsx b/components/groups/components/__tests__/StepIndicator.test.tsx index 3f88dc65..6617753c 100644 --- a/components/groups/components/__tests__/StepIndicator.test.tsx +++ b/components/groups/components/__tests__/StepIndicator.test.tsx @@ -2,6 +2,9 @@ import {describe, test, expect, afterEach} from "bun:test" import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; import StepIndicator from '@/components/groups/components/StepIndicator'; +import matchers from "@testing-library/jest-dom/matchers"; + +expect.extend(matchers); describe('StepIndicator Component', () => { afterEach(cleanup) From 9654e43f7824e351a06ae91de1c78cd472ef2096 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:38:27 -0400 Subject: [PATCH 37/63] test: validator modal --- .../modals/__tests__/validatorModal.test.tsx | 54 +++++++++++++++++++ components/admins/modals/validatorModal.tsx | 6 +-- tests/mock.ts | 2 +- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 components/admins/modals/__tests__/validatorModal.test.tsx diff --git a/components/admins/modals/__tests__/validatorModal.test.tsx b/components/admins/modals/__tests__/validatorModal.test.tsx new file mode 100644 index 00000000..8c40fa6f --- /dev/null +++ b/components/admins/modals/__tests__/validatorModal.test.tsx @@ -0,0 +1,54 @@ +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import {render, screen, fireEvent, cleanup, waitFor, within} from '@testing-library/react'; +import { ValidatorDetailsModal } from '@/components/admins/modals/validatorModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import {mockActiveValidators} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const validator = mockActiveValidators[0]; +const modalId = 'test-modal'; +const admin = 'manifest1adminaddress'; + +function renderWithProps(props = {}) { + return renderWithChainProvider(); +} + +describe('ValidatorDetailsModal Component', () => { + afterEach(cleanup); + + test('renders modal with correct details', () => { + renderWithProps(); + expect(screen.getByText('Validator Details')).toBeInTheDocument(); + expect(screen.getByText('Validator One')).toBeInTheDocument(); + expect(screen.getByText('security1@foobar.com')).toBeInTheDocument(); + const detailsContainer = screen.getByLabelText('details') + expect(within(detailsContainer).getByText('details1')).toBeInTheDocument(); + }); + + test('updates input field correctly', () => { + renderWithProps(); + const input = screen.getByPlaceholderText('1000'); + fireEvent.change(input, { target: { value: 2000 } }); + expect(input).toHaveValue(2000); + }); + + test('enables update button when input is valid', () => { + renderWithProps(); + const input = screen.getByPlaceholderText('1000'); + fireEvent.change(input, { target: { value: 2000 } }); + const updateButton = screen.getByText('update'); + expect(updateButton).toBeEnabled(); + }); + + // // TODO: Why is this test failing? + // // https://github.com/capricorn86/haVyppy-dom/issues/1184 + // test('closes modal when close button is clicked', async () => { + // renderWithProps(); + // const closeButton = screen.getByText('✕'); + // fireEvent.click(closeButton); + // await waitFor(() => expect(screen.queryByText('Validator Details')).not.toBeInTheDocument()); + // }); +}); \ No newline at end of file diff --git a/components/admins/modals/validatorModal.tsx b/components/admins/modals/validatorModal.tsx index cbf72e4e..c8741f74 100644 --- a/components/admins/modals/validatorModal.tsx +++ b/components/admins/modals/validatorModal.tsx @@ -20,11 +20,11 @@ export function ValidatorDetailsModal({ validator, modalId, admin, -}: { +}: Readonly<{ validator: ExtendedValidatorSDKType | null; modalId: string; admin: string; -}) { +}>) { const [power, setPowerInput] = useState( validator?.consensus_power?.toString() || "" ); @@ -163,7 +163,7 @@ export function ValidatorDetailsModal({ )}
    - + {validator.description.details ? validator.description.details.substring(0, 50) + (validator.description.details.length > 50 ? "..." : "") diff --git a/tests/mock.ts b/tests/mock.ts index 2ad40898..24983514 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -46,7 +46,7 @@ export const mockBalances: CombinedBalanceInfo[] = [ export const mockActiveValidators: ExtendedValidatorSDKType[] = [ { operator_address: "validator1", - description: { moniker: "Validator One", identity: "identity1", details: "details1", website: "website1.com", security_contact: "security1" }, + description: { moniker: "Validator One", identity: "identity1", details: "details1", website: "website1.com", security_contact: "security1@foobar.com" }, consensus_power: BigInt(1000), logo_url: "", jailed: false, From 15fa26d4f40398d071ca87eebde56764c9506146 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:55:14 -0400 Subject: [PATCH 38/63] test: warning modal --- .../modals/__tests__/warningModal.test.tsx | 47 +++++++++++++++++++ components/admins/modals/warningModal.tsx | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 components/admins/modals/__tests__/warningModal.test.tsx diff --git a/components/admins/modals/__tests__/warningModal.test.tsx b/components/admins/modals/__tests__/warningModal.test.tsx new file mode 100644 index 00000000..7e702c46 --- /dev/null +++ b/components/admins/modals/__tests__/warningModal.test.tsx @@ -0,0 +1,47 @@ +import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { WarningModal } from '@/components/admins/modals/warningModal'; +import matchers from '@testing-library/jest-dom/matchers'; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const admin = 'manifest1adminaddress'; +const address = 'manifest1validatoraddress'; +const moniker = 'Validator Moniker'; +const modalId = 'test-modal'; + +function renderWithProps(props = {}) { + return renderWithChainProvider(); +} + +describe('WarningModal Component', () => { + afterEach(cleanup); + + test('renders modal with correct details', () => { + renderWithProps(); + expect(screen.getByText('Are you sure you want to remove the validator')).toBeInTheDocument(); + expect(screen.getByText(moniker)).toBeInTheDocument(); + expect(screen.getByText('from the active set?')).toBeInTheDocument(); + }); + + test('displays correct text based on isActive prop', () => { + renderWithProps(); + expect(screen.getByText('Remove From Active Set')).toBeInTheDocument(); + + cleanup() + + renderWithProps({ isActive: false }); + expect(screen.getByText('Remove From Pending List')).toBeInTheDocument(); + }); + + // // TODO: Why is this test failing? + // // https://github.com/capricorn86/haVyppy-dom/issues/1184 + // test('closes modal when close button is clicked', async () => { + // renderWithProps(); + // const closeButton = screen.getByText('✕'); + // fireEvent.click(closeButton); + // await waitFor(() => expect(screen.queryByText('Are you sure you want to remove the validator')).not.toBeInTheDocument()); + // }); +}); \ No newline at end of file diff --git a/components/admins/modals/warningModal.tsx b/components/admins/modals/warningModal.tsx index 83880c89..cb57bf0f 100644 --- a/components/admins/modals/warningModal.tsx +++ b/components/admins/modals/warningModal.tsx @@ -21,7 +21,7 @@ export function WarningModal({ modalId, address, isActive, -}: WarningModalProps) { +}: Readonly) { const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); const { address: userAddress } = useChain(chainName); From b49f1cdee147f6508be45ace9e71332f5286fa2e Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:56:55 -0400 Subject: [PATCH 39/63] chore: cleanup --- .../admins/modals/__tests__/descriptionModal.test.tsx | 4 ++-- .../admins/modals/__tests__/updateAdminModal.test.tsx | 4 ++-- .../modals/__tests__/updateStakingParamsModal.test.tsx | 4 ++-- components/admins/modals/__tests__/validatorModal.test.tsx | 4 ++-- components/admins/modals/__tests__/warningModal.test.tsx | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/admins/modals/__tests__/descriptionModal.test.tsx b/components/admins/modals/__tests__/descriptionModal.test.tsx index 1e1f987f..9eecccdc 100644 --- a/components/admins/modals/__tests__/descriptionModal.test.tsx +++ b/components/admins/modals/__tests__/descriptionModal.test.tsx @@ -1,6 +1,6 @@ import {describe, test, afterEach, expect} from "bun:test"; import React from 'react'; -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { render, screen, cleanup } from '@testing-library/react'; import { DescriptionModal } from '@/components/admins/modals/descriptionModal'; import matchers from "@testing-library/jest-dom/matchers"; @@ -33,4 +33,4 @@ describe('DescriptionModal Component', () => { // fireEvent.click(closeButton); // await waitFor(() => expect(screen.queryByText(details)).not.toBeInTheDocument()); // }); -}); \ No newline at end of file +}); diff --git a/components/admins/modals/__tests__/updateAdminModal.test.tsx b/components/admins/modals/__tests__/updateAdminModal.test.tsx index ad6439e5..b8cd81f8 100644 --- a/components/admins/modals/__tests__/updateAdminModal.test.tsx +++ b/components/admins/modals/__tests__/updateAdminModal.test.tsx @@ -1,6 +1,6 @@ import { describe, test, afterEach, expect } from 'bun:test'; import React from 'react'; -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; import { UpdateAdminModal } from '@/components/admins/modals/updateAdminModal'; import matchers from '@testing-library/jest-dom/matchers'; import {renderWithChainProvider} from "@/tests/render"; @@ -60,4 +60,4 @@ describe('UpdateAdminModal Component', () => { // fireEvent.click(closeButton); // await waitFor(() => expect(screen.queryByText('Update Admin')).not.toBeInTheDocument()); // }); -}); \ No newline at end of file +}); diff --git a/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx index d1c31ea7..b525e048 100644 --- a/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx +++ b/components/admins/modals/__tests__/updateStakingParamsModal.test.tsx @@ -1,6 +1,6 @@ import { describe, test, afterEach, expect } from 'bun:test'; import React from 'react'; -import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; import { UpdateStakingParamsModal } from '@/components/admins/modals/updateStakingParamsModal'; import matchers from '@testing-library/jest-dom/matchers'; import {renderWithChainProvider} from "@/tests/render"; @@ -74,4 +74,4 @@ describe('UpdateStakingParamsModal Component', () => { // fireEvent.click(closeButton); // await waitFor(() => expect(screen.queryByText('Update Staking Parameters')).not.toBeInTheDocument()); // }); -}); \ No newline at end of file +}); diff --git a/components/admins/modals/__tests__/validatorModal.test.tsx b/components/admins/modals/__tests__/validatorModal.test.tsx index 8c40fa6f..51d7d93c 100644 --- a/components/admins/modals/__tests__/validatorModal.test.tsx +++ b/components/admins/modals/__tests__/validatorModal.test.tsx @@ -1,6 +1,6 @@ import { describe, test, afterEach, expect } from 'bun:test'; import React from 'react'; -import {render, screen, fireEvent, cleanup, waitFor, within} from '@testing-library/react'; +import {screen, fireEvent, cleanup, within} from '@testing-library/react'; import { ValidatorDetailsModal } from '@/components/admins/modals/validatorModal'; import matchers from '@testing-library/jest-dom/matchers'; import {mockActiveValidators} from "@/tests/mock"; @@ -51,4 +51,4 @@ describe('ValidatorDetailsModal Component', () => { // fireEvent.click(closeButton); // await waitFor(() => expect(screen.queryByText('Validator Details')).not.toBeInTheDocument()); // }); -}); \ No newline at end of file +}); diff --git a/components/admins/modals/__tests__/warningModal.test.tsx b/components/admins/modals/__tests__/warningModal.test.tsx index 7e702c46..0334fdbb 100644 --- a/components/admins/modals/__tests__/warningModal.test.tsx +++ b/components/admins/modals/__tests__/warningModal.test.tsx @@ -1,6 +1,6 @@ -import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; +import { describe, test, afterEach, expect } from 'bun:test'; import React from 'react'; -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { screen, cleanup } from '@testing-library/react'; import { WarningModal } from '@/components/admins/modals/warningModal'; import matchers from '@testing-library/jest-dom/matchers'; import {renderWithChainProvider} from "@/tests/render"; @@ -44,4 +44,4 @@ describe('WarningModal Component', () => { // fireEvent.click(closeButton); // await waitFor(() => expect(screen.queryByText('Are you sure you want to remove the validator')).not.toBeInTheDocument()); // }); -}); \ No newline at end of file +}); From 1cb4ce7b4b3b36cf7482a1179fcc3934d9cdbc6f Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:15:08 -0400 Subject: [PATCH 40/63] test: bank form --- .../bank/forms/__tests__/ibcSendForm.test.tsx | 80 +++++++++++++++++++ .../bank/forms/__tests__/sendForm.test.tsx | 79 ++++++++++++++++++ components/bank/forms/ibcSendForm.tsx | 9 ++- components/bank/forms/sendForm.tsx | 6 +- 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 components/bank/forms/__tests__/ibcSendForm.test.tsx create mode 100644 components/bank/forms/__tests__/sendForm.test.tsx diff --git a/components/bank/forms/__tests__/ibcSendForm.test.tsx b/components/bank/forms/__tests__/ibcSendForm.test.tsx new file mode 100644 index 00000000..f53e1a1c --- /dev/null +++ b/components/bank/forms/__tests__/ibcSendForm.test.tsx @@ -0,0 +1,80 @@ +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import {screen, cleanup, fireEvent, within} from '@testing-library/react'; +import IbcSendForm from '@/components/bank/forms/ibcSendForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import {mockBalances} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +function renderWithProps(props = {}) { + const defaultProps = { + address: 'manifest1address', + destinationChain: 'osmosis', + balances: mockBalances, + isBalancesLoading: false, + refetchBalances: jest.fn(), + }; + + return renderWithChainProvider(); +} + +// TODO: Validate form inputs in component +describe('IbcSendForm Component', () => { + afterEach(cleanup); + + test('renders form with correct details', () => { + renderWithProps(); + expect(screen.getByText('Token')).toBeInTheDocument(); + expect(screen.getByText('Recipient')).toBeInTheDocument(); + expect(screen.getByText('Amount')).toBeInTheDocument(); + }); + + test('empty balances', () => { + renderWithProps({balances: []}); + expect(screen.getByText('Select Token')).toBeInTheDocument(); + }); + + test('updates token dropdown correctly', () => { + renderWithProps(); + const dropdownLabelContainer = screen.getByLabelText("dropdown-label") + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + + const balanceContainer = screen.getByLabelText("Token 1"); + expect(within(balanceContainer).getByText('TOKEN 1')).toBeInTheDocument(); + expect(within(balanceContainer).queryByText('TOKEN 2')).not.toBeInTheDocument(); + }); + + test('updates recipient input correctly', () => { + renderWithProps(); + const recipientInput = screen.getByPlaceholderText('Recipient address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); + }); + + test('updates amount input correctly', () => { + renderWithProps(); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); + }); + + // // TODO: Make this test pass + // test('send button is disabled when inputs are invalid', () => { + // renderWithProps(); + // const sendButton = screen.getByLabelText('send-btn'); + // expect(sendButton).toBeDisabled(); + // }); + + // TODO: Fix inputs to be valid + test('send button is enabled when inputs are valid', () => { + renderWithProps(); + fireEvent.change(screen.getByPlaceholderText('Recipient address'), { target: { value: 'cosmos1recipient' } }); + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { target: { value: '100' } }); + const dropdownLabelContainer = screen.getByLabelText("dropdown-label") + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + const sendButton = screen.getByText('Send'); + expect(sendButton).toBeEnabled(); + }); +}); \ No newline at end of file diff --git a/components/bank/forms/__tests__/sendForm.test.tsx b/components/bank/forms/__tests__/sendForm.test.tsx new file mode 100644 index 00000000..8d4f512c --- /dev/null +++ b/components/bank/forms/__tests__/sendForm.test.tsx @@ -0,0 +1,79 @@ +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor, within } from '@testing-library/react'; +import SendForm from '@/components/bank/forms/sendForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { mockBalances } from '@/tests/mock'; +import { renderWithChainProvider } from '@/tests/render'; + +expect.extend(matchers); + +function renderWithProps(props = {}) { + const defaultProps = { + address: 'manifest1address', + balances: mockBalances, + isBalancesLoading: false, + refetchBalances: jest.fn(), + }; + + return renderWithChainProvider(); +} + +// TODO: Validate form inputs in component +describe('SendForm Component', () => { + afterEach(cleanup); + + test('renders form with correct details', () => { + renderWithProps(); + expect(screen.getByText('Token')).toBeInTheDocument(); + expect(screen.getByText('Recipient')).toBeInTheDocument(); + expect(screen.getByText('Amount')).toBeInTheDocument(); + }); + + test('empty balances', () => { + renderWithProps({balances: []}); + expect(screen.getByText('Select Token')).toBeInTheDocument(); + }); + + test('updates token dropdown correctly', () => { + renderWithProps(); + const dropdownLabelContainer = screen.getByLabelText('dropdown-label'); + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + + const balanceContainer = screen.getByLabelText("Token 1"); + expect(within(balanceContainer).getByText('TOKEN 1')).toBeInTheDocument(); + expect(within(balanceContainer).queryByText('TOKEN 2')).not.toBeInTheDocument(); + }); + + test('updates recipient input correctly', () => { + renderWithProps(); + const recipientInput = screen.getByPlaceholderText('Recipient address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); + }); + + test('updates amount input correctly', () => { + renderWithProps(); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); + }); + + // TODO: Make this test pass + // test('send button is disabled when inputs are invalid', () => { + // renderWithProps(); + // const sendButton = screen.getByText('Send'); + // expect(sendButton).toBeDisabled(); + // }); + + // TODO: Fix inputs to be valid + test('send button is enabled when inputs are valid', () => { + renderWithProps(); + fireEvent.change(screen.getByPlaceholderText('Recipient address'), { target: { value: 'cosmos1recipient' } }); + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { target: { value: '100' } }); + const dropdownLabelContainer = screen.getByLabelText("dropdown-label") + fireEvent.click(within(dropdownLabelContainer).getByText('TOKEN 1')); + const sendButton = screen.getByText('Send'); + expect(sendButton).toBeEnabled(); + }); +}); \ No newline at end of file diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 1cfd55de..48eadfdf 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -15,13 +15,13 @@ export default function IbcSendForm({ balances, isBalancesLoading, refetchBalances, -}: { +}: Readonly<{ address: string; destinationChain: string; balances: CombinedBalanceInfo[]; isBalancesLoading: boolean; refetchBalances: () => void; -}) { +}>) { const [recipient, setRecipient] = useState(""); const [amount, setAmount] = useState(""); const [selectedToken, setSelectedToken] = @@ -105,10 +105,11 @@ export default function IbcSendForm({ -
    +
    @@ -202,6 +204,7 @@ export default function IbcSendForm({ onClick={handleSend} className="btn btn-primary w-full" disabled={isSending} + aria-label="send-btn" > {isSending ? ( diff --git a/components/bank/forms/sendForm.tsx b/components/bank/forms/sendForm.tsx index ec9957be..e41e3fe7 100644 --- a/components/bank/forms/sendForm.tsx +++ b/components/bank/forms/sendForm.tsx @@ -12,12 +12,12 @@ export default function SendForm({ balances, isBalancesLoading, refetchBalances, -}: { +}: Readonly<{ address: string; balances: CombinedBalanceInfo[]; isBalancesLoading: boolean; refetchBalances: () => void; -}) { +}>) { const [recipient, setRecipient] = useState(""); const [amount, setAmount] = useState(""); const [selectedToken, setSelectedToken] = @@ -86,6 +86,7 @@ export default function SendForm({ From 455fb6d838309798325cdcdefe7aa967fd671b3c Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:54:50 -0400 Subject: [PATCH 41/63] test: burn form --- components/factory/forms/BurnForm.tsx | 5 +- .../factory/forms/__tests__/BurnForm.test.tsx | 74 +++++++++++++++++++ tests/mock.ts | 45 ++++++----- 3 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 components/factory/forms/__tests__/BurnForm.test.tsx diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index 1d643517..9ab0a5d6 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -22,14 +22,14 @@ export default function BurnForm({ address, refetch, balance, -}: { +}: Readonly<{ isAdmin: boolean; admin: string; denom: MetadataSDKType; address: string; refetch: () => void; balance: string; -}) { +}>) { const [amount, setAmount] = useState(""); const [recipient, setRecipient] = useState(address); const [isSigning, setIsSigning] = useState(false); @@ -263,6 +263,7 @@ export default function BurnForm({ diff --git a/components/factory/forms/__tests__/BurnForm.test.tsx b/components/factory/forms/__tests__/BurnForm.test.tsx new file mode 100644 index 00000000..bce871f7 --- /dev/null +++ b/components/factory/forms/__tests__/BurnForm.test.tsx @@ -0,0 +1,74 @@ +import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import BurnForm from '@/components/factory/forms/BurnForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import {mockDenomMeta1, mockMfxDenom} from "@/tests/mock"; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const mockProps = { + isAdmin: true, + admin: 'cosmos1adminaddress', + denom: mockDenomMeta1, + address: 'cosmos1address', + refetch: jest.fn(), + balance: '1000000', +}; + +function renderWithProps(props = {}) { + return renderWithChainProvider(); +} + +describe('BurnForm Component', () => { + afterEach(cleanup); + + test('renders form with correct details', () => { + renderWithProps(); + expect(screen.getByText('NAME')).toBeInTheDocument(); + expect(screen.getByText('YOUR BALANCE')).toBeInTheDocument(); + expect(screen.getByText('EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); + }); + + test('renders multi burn when token is mfx', () => { + renderWithProps({denom: mockMfxDenom}); + expect(screen.getByLabelText('multi-burn-btn')).toBeInTheDocument(); + }) + + test('renders not affiliated message when not admin and token is mfx', () => { + renderWithProps({isAdmin: false, denom: mockMfxDenom}); + expect(screen.getByText('You are not affiliated with any PoA Admin entity.')).toBeInTheDocument(); + }) + + test('updates amount input correctly', () => { + renderWithProps(); + const amountInput = screen.getByPlaceholderText('Enter amount'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); + }); + + test('updates recipient input correctly', () => { + renderWithProps(); + const recipientInput = screen.getByPlaceholderText('Target address'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); + }); + + // // TODO: Make this test pass + // test('burn button is disabled when inputs are invalid', () => { + // renderWithProps(); + // const burnButton = screen.getByText('Burn'); + // expect(burnButton).toBeDisabled(); + // }); + + // TODO: Validate form inputs in component + test('burn button is enabled when inputs are valid', () => { + renderWithProps(); + fireEvent.change(screen.getByPlaceholderText('Enter amount'), { target: { value: '100' } }); + fireEvent.change(screen.getByPlaceholderText('Target address'), { target: { value: 'cosmos1recipient' } }); + const burnButton = screen.getByText('Burn'); + expect(burnButton).toBeEnabled(); + }); +}); \ No newline at end of file diff --git a/tests/mock.ts b/tests/mock.ts index 24983514..54da1c4c 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -8,37 +8,42 @@ import { ProposalSDKType, ProposalStatus } from "@chalabi/manifestjs/dist/codegen/cosmos/group/v1/types"; +import {MetadataSDKType} from "@chalabi/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank"; + +export const mockDenomMeta1: MetadataSDKType = { + description: "My First Token", + name: "Token 1", + symbol: "TK1", + uri: "", + uri_hash: "", + display: "Token 1", + base: "token1", + denom_units: [{ denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, { denom: "token1", exponent: 6, aliases: ["token1"] }], +} + +export const mockDenomMeta2: MetadataSDKType = { + description: "My Second Token", + name: "Token 2", + symbol: "TK2", + uri: "", + uri_hash: "", + display: "Token 2", + base: "token2", + denom_units: [{ denom: "utoken2", exponent: 0, aliases: ["utoken2"] }, { denom: "token2", exponent: 6, aliases: ["token2"] }], +} export const mockBalances: CombinedBalanceInfo[] = [ { denom: "token1", coreDenom: "utoken1", amount: "1000", - metadata: { - description: "My First Token", - name: "Token 1", - symbol: "TK1", - uri: "", - uri_hash: "", - display: "Token 1", - base: "token1", - denom_units: [{ denom: "utoken1", exponent: 0, aliases: ["utoken1"] }, { denom: "token1", exponent: 6, aliases: ["token1"] }], - }, + metadata: mockDenomMeta1, }, { denom: "token2", coreDenom: "utoken2", amount: "2000", - metadata: { - description: "My Second Token", - name: "Token 2", - symbol: "TK2", - uri: "", - uri_hash: "", - display: "Token 2", - base: "token2", - denom_units: [{ denom: "utoken2", exponent: 0, aliases: ["utoken2"] }, { denom: "token2", exponent: 6, aliases: ["token2"] }], - }, + metadata: mockDenomMeta2, }, ]; From 9eb794a322d714315a72315c05d4cb451737ca68 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:12:32 -0400 Subject: [PATCH 42/63] ci: maybe fix coverage --- .github/workflows/test-coverage.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 7f5f95a5..a62dd6c7 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -6,14 +6,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install - name: Run tests and generate coverage run: bun run test:coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + file: ./coverage/lcov.info + token: ${{ secrets.CODECOV_TOKEN }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 26e902f3381ec31971bd8d4c86b664466946df8a Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:38:51 -0400 Subject: [PATCH 43/63] test: confirmation form --- ...frimationForm.tsx => ConfirmationForm.tsx} | 6 +- .../forms/__tests__/ConfirmationForm.test.tsx | 101 ++++++++++++++++++ components/factory/forms/index.ts | 2 +- pages/factory/create.tsx | 2 +- 4 files changed, 107 insertions(+), 4 deletions(-) rename components/factory/forms/{ConfrimationForm.tsx => ConfirmationForm.tsx} (97%) create mode 100644 components/factory/forms/__tests__/ConfirmationForm.test.tsx diff --git a/components/factory/forms/ConfrimationForm.tsx b/components/factory/forms/ConfirmationForm.tsx similarity index 97% rename from components/factory/forms/ConfrimationForm.tsx rename to components/factory/forms/ConfirmationForm.tsx index e2d0216d..9f04fb87 100644 --- a/components/factory/forms/ConfrimationForm.tsx +++ b/components/factory/forms/ConfirmationForm.tsx @@ -10,12 +10,12 @@ export default function ConfirmationForm({ prevStep, formData, address, -}: { +}: Readonly<{ nextStep: () => void; prevStep: () => void; formData: TokenFormData; address: string; -}) { +}>) { const [isSigning, setIsSigning] = useState(false); const { tx } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); @@ -26,6 +26,8 @@ export default function ConfirmationForm({ const fullDenom = `factory/${address}/${formData.subdenom}`; + // TODO: Verify `formData.denomUnits` is an array with at least 2 elements + const handleConfirm = async () => { setIsSigning(true); try { diff --git a/components/factory/forms/__tests__/ConfirmationForm.test.tsx b/components/factory/forms/__tests__/ConfirmationForm.test.tsx new file mode 100644 index 00000000..6dbdc97d --- /dev/null +++ b/components/factory/forms/__tests__/ConfirmationForm.test.tsx @@ -0,0 +1,101 @@ +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import ConfirmationForm from '@/components/factory/forms/ConfirmationForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import {renderWithChainProvider} from "@/tests/render"; + +expect.extend(matchers); + +const mockFormData = { + name: 'Name Test Token', + symbol: 'STT', + display: 'Display Test Token', + subdenom: 'subtesttoken', + description: 'This is a test token', + denomUnits: [ + { denom: 'testtoken', exponent: 0, aliases: [] }, + { denom: 'tt', exponent: 6, aliases: [] }, + ], + uri: 'www.someuri.com', + uriHash: 's0m3h4sh', + exponent: "6", + label: "LabelTT", + base: "BaseTT" +}; + +function renderWithProps(props = {}) { + const mockProps = { + nextStep: jest.fn(), + prevStep: jest.fn(), + formData: mockFormData, + address: 'cosmos1address', + }; + + return renderWithChainProvider(); +} + +describe('ConfirmationForm Component', () => { + afterEach(cleanup); + + // TODO: Fix hardcoded values in component + test('renders form with correct details', () => { + renderWithProps(); + expect(screen.getByText('Token Information')).toBeInTheDocument(); + expect(screen.getByText('Token Name')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.name)).toBeInTheDocument(); + + expect(screen.getByText('Symbol')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.symbol)).toBeInTheDocument(); + + expect(screen.getByText('Display')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.display)).toBeInTheDocument(); + + expect(screen.getByText('Subdenom')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.subdenom)).toBeInTheDocument(); + + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.description)).toBeInTheDocument(); + + expect(screen.getByText('Denom Units')).toBeInTheDocument(); + expect(screen.getByText('Base Denom')).toBeInTheDocument(); + // TODO: Fix the following. This is hardcoded to `turd` at the moment. + // expect(screen.getByText(mockFormData.denomUnits[0].denom)).toBeInTheDocument(); + + expect(screen.getByText('Base Exponent')).toBeInTheDocument(); + // TODO: Fix the following. This is hardcoded to `0` at the moment. + // expect(screen.getByText(mockFormData.denomUnits[0].exponent.toString())).toBeInTheDocument(); + + expect(screen.getByText('Full Denom')).toBeInTheDocument(); + expect(screen.getByText(`factory/cosmos1address/${mockFormData.subdenom}`)).toBeInTheDocument(); + + expect(screen.getByText('Full Denom Exponent')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.denomUnits[1].exponent.toString())).toBeInTheDocument(); + }); + + // TODO: Fix advanced details in component + test('toggles advanced details correctly', () => { + renderWithProps(); + const toggleButton = screen.getByText('Show Advanced Details'); + fireEvent.click(toggleButton); + + expect(screen.getByText('URI')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.uri)).toBeInTheDocument(); + + expect(screen.getByText('URI Hash')).toBeInTheDocument(); + expect(screen.getByText(mockFormData.uriHash)).toBeInTheDocument(); + + expect(screen.getByText('Base Denom Alias')).toBeInTheDocument(); + // TODO: Fix the following in component. This should be the alias, not the subdenom. + // expect(screen.getByText(mockFormData.subdenom)).toBeInTheDocument(); + + expect(screen.getByText('Full Denom Alias')).toBeInTheDocument(); + // TODO: Fix the following in component. This should be the alias, not the display. + // expect(screen.getByText(mockFormData.display)).toBeInTheDocument(); + + fireEvent.click(toggleButton); + + expect(screen.queryByText('URI')).not.toBeInTheDocument(); + expect(screen.queryByText('URI Hash')).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/components/factory/forms/index.ts b/components/factory/forms/index.ts index d811f5c6..52051e54 100644 --- a/components/factory/forms/index.ts +++ b/components/factory/forms/index.ts @@ -1,4 +1,4 @@ -export * from './ConfrimationForm' +export * from './ConfirmationForm' export * from './CreateDenom' export * from './Success' export * from './TokenDetailsForm' diff --git a/pages/factory/create.tsx b/pages/factory/create.tsx index 41d4f459..f1c5281f 100644 --- a/pages/factory/create.tsx +++ b/pages/factory/create.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useReducer } from "react"; import { tokenFormDataReducer, TokenFormData } from "@/helpers/formReducer"; -import ConfirmationForm from "@/components/factory/forms/ConfrimationForm"; +import ConfirmationForm from "@/components/factory/forms/ConfirmationForm"; import TokenDetails from "@/components/factory/forms/TokenDetailsForm"; import { Duration } from "@chalabi/manifestjs/dist/codegen/google/protobuf/duration"; From 10f472305b6101c5fd4594dcbc8c5f725ffe4b22 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:11:32 -0400 Subject: [PATCH 44/63] test: cleanup burn form import --- components/factory/forms/__tests__/BurnForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/factory/forms/__tests__/BurnForm.test.tsx b/components/factory/forms/__tests__/BurnForm.test.tsx index bce871f7..b9cc5eb8 100644 --- a/components/factory/forms/__tests__/BurnForm.test.tsx +++ b/components/factory/forms/__tests__/BurnForm.test.tsx @@ -1,6 +1,6 @@ import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; import React from 'react'; -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; import BurnForm from '@/components/factory/forms/BurnForm'; import matchers from '@testing-library/jest-dom/matchers'; import {mockDenomMeta1, mockMfxDenom} from "@/tests/mock"; From 8353a2b8d96e2371ca69b321272be2a1501a823e Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:12:28 -0400 Subject: [PATCH 45/63] test: extract mock from confirmation form to re-use --- .../forms/__tests__/ConfirmationForm.test.tsx | 42 ++++++------------- tests/mock.ts | 18 ++++++++ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/components/factory/forms/__tests__/ConfirmationForm.test.tsx b/components/factory/forms/__tests__/ConfirmationForm.test.tsx index 6dbdc97d..76632cd1 100644 --- a/components/factory/forms/__tests__/ConfirmationForm.test.tsx +++ b/components/factory/forms/__tests__/ConfirmationForm.test.tsx @@ -1,34 +1,18 @@ -import { describe, test, afterEach, expect, jest } from 'bun:test'; +import {afterEach, describe, expect, jest, test} from 'bun:test'; import React from 'react'; -import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import {cleanup, fireEvent, screen} from '@testing-library/react'; import ConfirmationForm from '@/components/factory/forms/ConfirmationForm'; import matchers from '@testing-library/jest-dom/matchers'; import {renderWithChainProvider} from "@/tests/render"; +import {mockTokenFormData} from "@/tests/mock"; expect.extend(matchers); -const mockFormData = { - name: 'Name Test Token', - symbol: 'STT', - display: 'Display Test Token', - subdenom: 'subtesttoken', - description: 'This is a test token', - denomUnits: [ - { denom: 'testtoken', exponent: 0, aliases: [] }, - { denom: 'tt', exponent: 6, aliases: [] }, - ], - uri: 'www.someuri.com', - uriHash: 's0m3h4sh', - exponent: "6", - label: "LabelTT", - base: "BaseTT" -}; - function renderWithProps(props = {}) { const mockProps = { nextStep: jest.fn(), prevStep: jest.fn(), - formData: mockFormData, + formData: mockTokenFormData, address: 'cosmos1address', }; @@ -43,19 +27,19 @@ describe('ConfirmationForm Component', () => { renderWithProps(); expect(screen.getByText('Token Information')).toBeInTheDocument(); expect(screen.getByText('Token Name')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.name)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.name)).toBeInTheDocument(); expect(screen.getByText('Symbol')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.symbol)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.symbol)).toBeInTheDocument(); expect(screen.getByText('Display')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.display)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.display)).toBeInTheDocument(); expect(screen.getByText('Subdenom')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.subdenom)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.subdenom)).toBeInTheDocument(); expect(screen.getByText('Description')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.description)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.description)).toBeInTheDocument(); expect(screen.getByText('Denom Units')).toBeInTheDocument(); expect(screen.getByText('Base Denom')).toBeInTheDocument(); @@ -67,10 +51,10 @@ describe('ConfirmationForm Component', () => { // expect(screen.getByText(mockFormData.denomUnits[0].exponent.toString())).toBeInTheDocument(); expect(screen.getByText('Full Denom')).toBeInTheDocument(); - expect(screen.getByText(`factory/cosmos1address/${mockFormData.subdenom}`)).toBeInTheDocument(); + expect(screen.getByText(`factory/cosmos1address/${mockTokenFormData.subdenom}`)).toBeInTheDocument(); expect(screen.getByText('Full Denom Exponent')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.denomUnits[1].exponent.toString())).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString())).toBeInTheDocument(); }); // TODO: Fix advanced details in component @@ -80,10 +64,10 @@ describe('ConfirmationForm Component', () => { fireEvent.click(toggleButton); expect(screen.getByText('URI')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.uri)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.uri)).toBeInTheDocument(); expect(screen.getByText('URI Hash')).toBeInTheDocument(); - expect(screen.getByText(mockFormData.uriHash)).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.uriHash)).toBeInTheDocument(); expect(screen.getByText('Base Denom Alias')).toBeInTheDocument(); // TODO: Fix the following in component. This should be the alias, not the subdenom. diff --git a/tests/mock.ts b/tests/mock.ts index 54da1c4c..f0a25451 100644 --- a/tests/mock.ts +++ b/tests/mock.ts @@ -403,3 +403,21 @@ export const mockProposals: { [key: string]: ProposalSDKType[] } = { } ] }; + +// TODO: Re-use mockDenomMeta1 here +export const mockTokenFormData = { + name: 'Name Test Token', + symbol: 'STT', + display: 'Display Test Token', + subdenom: 'subtesttoken', + description: 'This is a test token', + denomUnits: [ + {denom: 'testtoken', exponent: 0, aliases: []}, + {denom: 'tt', exponent: 6, aliases: []}, + ], + uri: 'www.someuri.com', + uriHash: 's0m3h4sh', + exponent: "6", + label: "LabelTT", + base: "BaseTT" +}; \ No newline at end of file From 6bf52f6b1b8b91fa64c853bc4687da4126919a67 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:12:47 -0400 Subject: [PATCH 46/63] test: mint and create denom form --- components/factory/forms/CreateDenom.tsx | 5 +- components/factory/forms/MintForm.tsx | 6 +- .../forms/__tests__/CreateDenom.test.tsx | 60 ++++++++++++++++++ .../factory/forms/__tests__/MintForm.test.tsx | 61 +++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 components/factory/forms/__tests__/CreateDenom.test.tsx create mode 100644 components/factory/forms/__tests__/MintForm.test.tsx diff --git a/components/factory/forms/CreateDenom.tsx b/components/factory/forms/CreateDenom.tsx index d143560f..91198bfe 100644 --- a/components/factory/forms/CreateDenom.tsx +++ b/components/factory/forms/CreateDenom.tsx @@ -9,12 +9,12 @@ export default function CreateDenom({ formData, dispatch, address, -}: { +}: Readonly<{ nextStep: () => void; formData: TokenFormData; dispatch: React.Dispatch; address: string; -}) { +}>) { const [error, setError] = useState(null); const [touched, setTouched] = useState(false); @@ -98,6 +98,7 @@ export default function CreateDenom({ void; balance: string; isAdmin: boolean; -}) { +}>) { const [amount, setAmount] = useState(""); const [recipient, setRecipient] = useState(address); const [isSigning, setIsSigning] = useState(false); @@ -223,6 +223,7 @@ export default function MintForm({

    AMOUNT

    { + afterEach(cleanup); + + test('renders form with correct details', () => { + renderWithChainProvider(); + expect(screen.getByText('Create Denom')).toBeInTheDocument(); + expect(screen.getByText('Token Sub Denom')).toBeInTheDocument(); + }); + + // TODO: Make this test pass. Why is the input not being updated? + // test('updates subdenom input correctly', () => { + // renderWithChainProvider(); + // const subdenomInput = screen.getByPlaceholderText('udenom'); + // const subdenomInput = screen.getByLabelText('denom-input'); + // fireEvent.change(subdenomInput, { target: { value: 'utest' } }); + // expect(subdenomInput).toHaveValue('utest'); + // }); + + // TODO: Make this test pass. Why is the input not being updated? + // test('shows validation error for invalid subdenom', () => { + // renderWithChainProvider(); + // const subdenomInput = screen.getByPlaceholderText('udenom'); + // fireEvent.change(subdenomInput, { target: { value: '1invalid' } }); + // fireEvent.blur(subdenomInput); + // expect(screen.getByText('Subdenom must start with a letter')).toBeInTheDocument(); + // }); + // + // // TODO: The confirm button should be disabled when the input is invalid + // test('confirm button is disabled when inputs are invalid', () => { + // renderWithChainProvider(); + // const confirmButton = screen.getByText('Next: Token Metadata'); + // expect(confirmButton).toBeDisabled(); + // }); + // + // // TODO: The confirm button should be enabled when the input is valid + // test('confirm button is enabled when inputs are valid', () => { + // renderWithChainProvider(); + // const subdenomInput = screen.getByPlaceholderText('udenom'); + // fireEvent.change(subdenomInput, { target: { value: 'utest' } }); + // const confirmButton = screen.getByText('Next: Token Metadata'); + // expect(confirmButton).toBeEnabled(); + // }); +}); \ No newline at end of file diff --git a/components/factory/forms/__tests__/MintForm.test.tsx b/components/factory/forms/__tests__/MintForm.test.tsx new file mode 100644 index 00000000..4e93bca9 --- /dev/null +++ b/components/factory/forms/__tests__/MintForm.test.tsx @@ -0,0 +1,61 @@ +import { describe, test, afterEach, expect, jest } from 'bun:test'; +import React from 'react'; +import { screen, fireEvent, cleanup } from '@testing-library/react'; +import MintForm from '@/components/factory/forms/MintForm'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import {mockDenomMeta1} from "@/tests/mock"; + +expect.extend(matchers); + +const mockProps = { + isAdmin: true, + admin: 'cosmos1adminaddress', + denom: mockDenomMeta1, + address: 'cosmos1address', + refetch: jest.fn(), + balance: '1000000', +}; + +describe('MintForm Component', () => { + afterEach(cleanup); + + test('renders form with correct details', () => { + renderWithChainProvider(); + expect(screen.getByText('NAME')).toBeInTheDocument(); + expect(screen.getByText('YOUR BALANCE')).toBeInTheDocument(); + expect(screen.getByText('EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('CIRCULATING SUPPLY')).toBeInTheDocument(); + }); + + test('updates amount input correctly', () => { + renderWithChainProvider(); + const amountInput = screen.getByLabelText('mint-amount-input'); + fireEvent.change(amountInput, { target: { value: '100' } }); + expect(amountInput).toHaveValue('100'); + }); + + test('updates recipient input correctly', () => { + renderWithChainProvider(); + const recipientInput = screen.getByLabelText('mint-recipient-input'); + fireEvent.change(recipientInput, { target: { value: 'cosmos1recipient' } }); + expect(recipientInput).toHaveValue('cosmos1recipient'); + }); + + // TODO: Button is disabled when inputs are invalid + // test('mint button is disabled when inputs are invalid', () => { + // renderWithChainProvider(); + // const mintButton = screen.getByText('Mint'); + // expect(mintButton).toBeDisabled(); + // }); + // + // TODO: Button is enabled when inputs are valid + // Fix values validation in the component, this test should not pass as-is + test('mint button is enabled when inputs are valid', () => { + renderWithChainProvider(); + fireEvent.change(screen.getByLabelText('mint-amount-input'), { target: { value: '100' } }); + fireEvent.change(screen.getByLabelText('mint-recipient-input'), { target: { value: 'cosmos1recipient' } }); + const mintButton = screen.getByText('Mint'); + expect(mintButton).toBeEnabled(); + }); +}); \ No newline at end of file From b1caf4c9902db4e7933fbcc27c1f126c7d3911d7 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:23:13 -0400 Subject: [PATCH 47/63] test: cleanup import --- components/factory/forms/__tests__/BurnForm.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/factory/forms/__tests__/BurnForm.test.tsx b/components/factory/forms/__tests__/BurnForm.test.tsx index b9cc5eb8..e397ccc7 100644 --- a/components/factory/forms/__tests__/BurnForm.test.tsx +++ b/components/factory/forms/__tests__/BurnForm.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, afterEach, expect, jest, mock } from 'bun:test'; +import { describe, test, afterEach, expect, jest } from 'bun:test'; import React from 'react'; import { screen, fireEvent, cleanup } from '@testing-library/react'; import BurnForm from '@/components/factory/forms/BurnForm'; From 5c8b17bce859179a85fc6d57cf250c4a679d736d Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:23:26 -0400 Subject: [PATCH 48/63] test: success form --- components/factory/forms/Success.tsx | 12 +++-- .../factory/forms/__tests__/Success.test.tsx | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 components/factory/forms/__tests__/Success.test.tsx diff --git a/components/factory/forms/Success.tsx b/components/factory/forms/Success.tsx index e664a5f1..85ae385a 100644 --- a/components/factory/forms/Success.tsx +++ b/components/factory/forms/Success.tsx @@ -5,10 +5,10 @@ import Link from "next/link"; export default function Success({ formData, address, -}: { +}: Readonly<{ formData: TokenFormData; address: string; -}) { +}>) { const fullDenom = `factory/${address}/${formData.subdenom}`; return ( @@ -22,12 +22,16 @@ export default function Success({ You can now mint, burn, or change the admin of your tokens and send them to other wallets.

    -

    + {/* + TODO: Verify the render is correct. + I changed the

    to a

    here because
    (in TruncatedAddressWithCopy) cannot be a descendant of

    + */} +

    The full denom of your token is:{" "} -

    +

    Token Details

    diff --git a/components/factory/forms/__tests__/Success.test.tsx b/components/factory/forms/__tests__/Success.test.tsx new file mode 100644 index 00000000..73ee0ab6 --- /dev/null +++ b/components/factory/forms/__tests__/Success.test.tsx @@ -0,0 +1,44 @@ +import { describe, test, afterEach, expect } from 'bun:test'; +import React from 'react'; +import { screen, cleanup } from '@testing-library/react'; +import Success from '@/components/factory/forms/Success'; +import matchers from '@testing-library/jest-dom/matchers'; +import { renderWithChainProvider } from '@/tests/render'; +import {mockTokenFormData} from "@/tests/mock"; + +expect.extend(matchers); + +const mockProps = { + formData: mockTokenFormData, + address: 'cosmos1address', +}; + +describe('Success Component', () => { + afterEach(cleanup); + + test('renders component with correct details', () => { + renderWithChainProvider(); + expect(screen.getByText('Success!')).toBeInTheDocument(); + expect(screen.getByText('Your token was successfully created and the metadata was set.')).toBeInTheDocument(); + expect(screen.getByText('The full denom of your token is:')).toBeInTheDocument(); + expect(screen.getByText('Token Details')).toBeInTheDocument(); + }); + + test('displays token details correctly', () => { + renderWithChainProvider(); + expect(screen.getByText('NAME')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.name)).toBeInTheDocument(); + expect(screen.getByText('SYMBOL')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.symbol)).toBeInTheDocument(); + expect(screen.getByText('DISPLAY')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.display)).toBeInTheDocument(); + expect(screen.getByText('SUBDENOM')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.subdenom)).toBeInTheDocument(); + expect(screen.getByText('DESCRIPTION')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.description)).toBeInTheDocument(); + expect(screen.getByText('BASE EXPONENT')).toBeInTheDocument(); + expect(screen.getByText('0')).toBeInTheDocument(); + expect(screen.getByText('DISPLAY EXPONENT')).toBeInTheDocument(); + expect(screen.getByText(mockTokenFormData.denomUnits[1].exponent.toString())).toBeInTheDocument(); + }); +}); \ No newline at end of file From 15aeb2c48ea611df31f656111fd86a62a5de9cf1 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:36:38 -0400 Subject: [PATCH 49/63] test: token detail and transfer form --- components/factory/forms/TokenDetailsForm.tsx | 11 ++- .../forms/__tests__/TokenDetailsForm.test.tsx | 77 +++++++++++++++++++ .../forms/__tests__/TransferForm.test.tsx | 62 +++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 components/factory/forms/__tests__/TokenDetailsForm.test.tsx create mode 100644 components/factory/forms/__tests__/TransferForm.test.tsx diff --git a/components/factory/forms/TokenDetailsForm.tsx b/components/factory/forms/TokenDetailsForm.tsx index 23347dfc..5512e6e2 100644 --- a/components/factory/forms/TokenDetailsForm.tsx +++ b/components/factory/forms/TokenDetailsForm.tsx @@ -9,13 +9,13 @@ export default function TokenDetails({ formData, dispatch, address, -}: { +}: Readonly<{ nextStep: () => void; prevStep: () => void; formData: TokenFormData; dispatch: React.Dispatch; address: string; -}) { +}>) { const updateField = (field: keyof TokenFormData, value: any) => { dispatch({ type: "UPDATE_FIELD", field, value }); }; @@ -69,6 +69,7 @@ export default function TokenDetails({ (Unique identifier) (Full name) (Short symbol) (Brief description)