diff --git a/src/components/Settings/ManageOwners/index.tsx b/src/components/Settings/ManageOwners/index.tsx index b08c1ad5..f176d8e4 100644 --- a/src/components/Settings/ManageOwners/index.tsx +++ b/src/components/Settings/ManageOwners/index.tsx @@ -1,3 +1,4 @@ +import BookIcon from "@mui/icons-material/Book"; import CloseIcon from "@mui/icons-material/Close"; import CreateOutlinedIcon from "@mui/icons-material/CreateOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; @@ -8,36 +9,45 @@ import { Divider, Modal, TextField, + Tooltip, Typography, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; +import router from "next/router"; import * as React from "react"; -import { ArrayOneOrMore } from "useink/dist/core"; import { useAppNotificationContext } from "@/components/AppToastNotification/AppNotificationsContext"; +import { LoadingButton } from "@/components/common/LoadingButton"; +import { LoadingSkeleton } from "@/components/common/LoadingSkeleton"; import NetworkBadge from "@/components/NetworkBadge"; import { AccountSigner } from "@/components/StepperSignersAccount/AccountSigner"; import { getChain } from "@/config/chain"; -import { Owner, SignatoriesAccount } from "@/domain/SignatoriesAccount"; +import { ROUTES } from "@/config/routes"; +import { + CrossOwnerWithAddressBook, + Owner, + SignatoriesAccount, +} from "@/domain/SignatoriesAccount"; import { useSetXsignerSelected } from "@/hooks/xsignerSelected/useSetXsignerSelected"; +interface Props { + selectedMultisig?: SignatoriesAccount; + handleAddOwner: () => void; + handleDeleteOwner: (owner: Owner) => void; + isDeletedLoading?: boolean; +} + export default function ManageOwners({ - owners, selectedMultisig, handleAddOwner, handleDeleteOwner, isDeletedLoading = false, -}: { - owners?: ArrayOneOrMore; - selectedMultisig?: SignatoriesAccount; - handleAddOwner: () => void; - handleDeleteOwner: (owner: Owner) => void; - isDeletedLoading?: boolean; -}) { +}: Props) { const theme = useTheme(); const [open, setOpen] = React.useState({ edit: false, delete: false }); + const { owners } = selectedMultisig || {}; const [ownersList, setOwnersList] = React.useState< - ArrayOneOrMore | undefined + SignatoriesAccount["owners"] | undefined >(owners); const [currentOwner, setCurrentOwner] = React.useState({} as Owner); const { setXsigner } = useSetXsignerSelected(); @@ -77,10 +87,12 @@ export default function ManageOwners({ await setXsigner({ ...selectedMultisig, - owners: newOwners as ArrayOneOrMore, + owners: newOwners as SignatoriesAccount["owners"], }); - setOwnersList(newOwners as ArrayOneOrMore); + setOwnersList( + newOwners as SignatoriesAccount["owners"] + ); handleClose(); addNotification({ message: "Owner name updated successfully", @@ -92,7 +104,11 @@ export default function ManageOwners({ + {ownersList === undefined && ( + + )} {ownersList?.map((owner) => ( - handleEdit(owner)} - /> + {owner.inAddressBook ? ( + + router.replace(ROUTES.AddressBook)} + /> + + ) : ( + handleEdit(owner)} + /> + )} {isDeletedLoading && currentOwner.address === owner.address ? ( @@ -304,7 +336,7 @@ export default function ManageOwners({ ))} - + ); diff --git a/src/domain/SignatoriesAccount.ts b/src/domain/SignatoriesAccount.ts index db956c2c..d8f7c7c9 100644 --- a/src/domain/SignatoriesAccount.ts +++ b/src/domain/SignatoriesAccount.ts @@ -7,10 +7,18 @@ export type Owner = { name: string; }; -export interface SignatoriesAccount { +export type CrossOwnerWithAddressBook = { + address: string; + name: string; + inAddressBook: boolean; +}; + +export interface SignatoriesAccount< + T extends Owner | CrossOwnerWithAddressBook = Owner +> { address: string; name: string; networkId: Chain["id"]; - owners: ArrayOneOrMore; + owners: ArrayOneOrMore; threshold: number; } diff --git a/src/hooks/addressBook/useAddAddressBook.ts b/src/hooks/addressBook/useAddAddressBook.ts index 8aa5955e..06491d92 100644 --- a/src/hooks/addressBook/useAddAddressBook.ts +++ b/src/hooks/addressBook/useAddAddressBook.ts @@ -1,17 +1,24 @@ import { ChangeEvent, useState } from "react"; +import { DEFAULT_CHAIN } from "@/config/chain"; import { useLocalDbContext } from "@/context/uselocalDbContext"; import { usePolkadotContext } from "@/context/usePolkadotContext"; import { AddressBook } from "@/domain/AddressBooks"; import { AddressBookEvents } from "@/domain/events/AddressBookEvents"; import { ChainId } from "@/services/useink/types"; -import { isValidAddress } from "@/utils/blockchain"; +import { + areAddressesEqual, + getHexAddress, + isValidAddress, +} from "@/utils/blockchain"; const VALIDATIONS = { isInputEmpty: (input: string) => input.trim() === "", isValidAddress: isValidAddress, existAddress: (accountAddress: string, data: AddressBook[]): boolean => - data.some((element) => element.address === accountAddress), + data.some((addressBookItem) => + areAddressesEqual(addressBookItem.address, accountAddress) + ), }; const ERROR_MESSAGES = { @@ -20,8 +27,6 @@ const ERROR_MESSAGES = { INVALID_ADDRESS: "This is not a valid address", }; -const DEFAULT_CHAIN = "rococo-contracts-testnet"; - const initialErrorState = { name: { isError: false, @@ -109,19 +114,19 @@ export const useAddAddressBook = () => { return; } - addAddress(formInput, network); + _addAddress(formInput, network); setError(initialErrorState); }; - const addAddress = ( + const _addAddress = ( addressBook: AddressBook | undefined, network: ChainId | undefined ) => { if (!addressBook) return; const newRegister: AddressBook = { networkId: network as ChainId, - name: addressBook?.name, - address: addressBook?.address, + name: addressBook.name, + address: getHexAddress(addressBook.address), }; addressBookRepository.addAddress(newRegister); @@ -136,7 +141,6 @@ export const useAddAddressBook = () => { }; return { - addAddress, handleChangeInput, handleClick, resetState, diff --git a/src/hooks/addressBook/useListAddressBook.ts b/src/hooks/addressBook/useListAddressBook.ts index 9d83b63f..356aa78a 100644 --- a/src/hooks/addressBook/useListAddressBook.ts +++ b/src/hooks/addressBook/useListAddressBook.ts @@ -5,6 +5,7 @@ import { usePolkadotContext } from "@/context/usePolkadotContext"; import { AddressBook } from "@/domain/AddressBooks"; import { AddressBookEvents } from "@/domain/events/AddressBookEvents"; import { useEventListenerCallback } from "@/hooks/useEventListenerCallback"; +import { formatAddressForNetwork } from "@/utils/blockchain"; import { customReportError } from "@/utils/error"; export function useListAddressBook(networkId: string | undefined) { @@ -23,6 +24,7 @@ export function useListAddressBook(networkId: string | undefined) { if (!result) return []; const newData = result.map((element) => ({ ...element, + address: formatAddressForNetwork(element.address, network), isEditable: false, })); setData(newData); diff --git a/src/hooks/xsignerSelected/useGetXsignerSelected.ts b/src/hooks/xsignerSelected/useGetXsignerSelected.ts index 2f6d0d2b..1b572f23 100644 --- a/src/hooks/xsignerSelected/useGetXsignerSelected.ts +++ b/src/hooks/xsignerSelected/useGetXsignerSelected.ts @@ -4,12 +4,21 @@ import { useLocalDbContext } from "@/context/uselocalDbContext"; import { usePolkadotContext } from "@/context/usePolkadotContext"; import { XsignerAccountEvents } from "@/domain/events/XsignerAccountEvents"; import { SignatoriesAccount } from "@/domain/SignatoriesAccount"; +import { createArrayOneOrMore } from "@/domain/utilityTsTypes"; +import { getHexAddress } from "@/utils/blockchain"; import { useEventListenerCallback } from "../useEventListenerCallback"; -export function useGetXsignerSelected() { - const { xsignerSelectedRepository, signatoriesAccountRepository } = - useLocalDbContext(); +interface UseGetXsignerSelectedReturn { + xSignerSelected: SignatoriesAccount | null | undefined; +} + +export function useGetXsignerSelected(): UseGetXsignerSelectedReturn { + const { + xsignerSelectedRepository, + signatoriesAccountRepository, + addressBookRepository, + } = useLocalDbContext(); const { network } = usePolkadotContext(); const [xSignerSelected, setXsignerSelected] = useState< SignatoriesAccount | null | undefined @@ -26,11 +35,29 @@ export function useGetXsignerSelected() { )); if (account) { - setXsignerSelected(account); + setXsignerSelected({ + ...account, + owners: createArrayOneOrMore( + account.owners.map((owner) => { + const inAddressBook = addressBookRepository.getItemByAddress( + getHexAddress(owner.address) + ); + + if (!inAddressBook) return { ...owner, inAddressBook: false }; + + return { ...owner, name: inAddressBook.name, inAddressBook: true }; + }) + ), + }); } else { setXsignerSelected(null); } - }, [network, signatoriesAccountRepository, xsignerSelectedRepository]); + }, [ + addressBookRepository, + network, + signatoriesAccountRepository, + xsignerSelectedRepository, + ]); useEffect(() => { getAccount(); diff --git a/src/pages/app/settings.tsx b/src/pages/app/settings.tsx index 8e4803e9..8db48bfc 100644 --- a/src/pages/app/settings.tsx +++ b/src/pages/app/settings.tsx @@ -300,7 +300,6 @@ export default function SettingsPage() { element.address === accountAddress ); diff --git a/src/utils/blockchain.ts b/src/utils/blockchain.ts index 95bf3043..87c1727e 100644 --- a/src/utils/blockchain.ts +++ b/src/utils/blockchain.ts @@ -115,15 +115,40 @@ const networkIdToPrefix: Record = { "rococo-contracts-testnet": 42, }; -export const areAddressesEqual = (address1: string, address2: string) => { - const rawAddress1 = ss58.decode(address1).bytes; - const rawAddress2 = ss58.decode(address2).bytes; +export const getHexAddress = (address: string) => ss58.decode(address).bytes; + +/** + * Checks if two Polkadot/Kusama addresses are equal. + * This is done by decoding the addresses to their raw byte representation + * and comparing these byte arrays. + * + * @param {string} address1 - The first address to compare. + * @param {string} address2 - The second address to compare. + * @returns {boolean} - Returns true if the addresses are equal, otherwise false. + */ +export const areAddressesEqual = ( + address1: string, + address2: string +): boolean => { + const rawAddress1 = getHexAddress(address1); + const rawAddress2 = getHexAddress(address2); return rawAddress1 === rawAddress2; }; +/** + * Formats a Polkadot/Kusama address for a specific network. + * This is achieved by decoding the address to its raw byte format, + * then re-encoding it with the prefix corresponding to the desired network. + * Each network in the Polkadot ecosystem has a specific prefix which denotes the network. + * + * @param {string} address - The address to format. + * @param {string} networkId - The network identifier for which to format the address. + * @returns {string} - The formatted address for the specified network. If the networkId is unknown, + * it defaults to using a prefix of 42, commonly used for generic or development purposes. + */ export const formatAddressForNetwork = (address: string, networkId: string) => { - const rawAddress = ss58.decode(address).bytes; + const rawAddress = isHex(address) ? address : ss58.decode(address).bytes; const prefix = networkIdToPrefix[networkId] !== undefined