Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add table enhancements and owner validation #46

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
11 changes: 9 additions & 2 deletions app/components/ClickableAddress.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Web3Provider } from '@ethersproject/providers';
import { useWeb3React } from '@web3-react/core';
import { FC } from 'react';
import { openInEtherscan } from 'utils/openInEtherscan';
import { openInEtherscan } from 'utils/utils';
import { truncateAddress } from 'utils/truncate';
import Jazzicon from './Jazzicon';

interface ClickableAddressProps {
address: string;
truncate?: boolean;
withJazzicon?: boolean;
}

export const ClickableAddress: FC<ClickableAddressProps> = ({ address, truncate = false }) => {
export const ClickableAddress: FC<ClickableAddressProps> = ({
address,
truncate = false,
withJazzicon = false,
}) => {
const { chainId } = useWeb3React<Web3Provider>();

return (
Expand All @@ -19,6 +25,7 @@ export const ClickableAddress: FC<ClickableAddressProps> = ({ address, truncate
title="Open on Etherscan"
>
{truncate ? truncateAddress(address) : address}
{withJazzicon && <Jazzicon address={address} />}
</div>
);
};
37 changes: 3 additions & 34 deletions app/components/ConfirmsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { FC, useEffect, useState, useContext } from 'react';
import { useWeb3React } from '@web3-react/core';
import { Web3Provider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
import { FC, useContext } from 'react';
import { Modal, ModalContext } from 'components/Modal';
import { abi as multisigAbi } from 'abi/MultiSigWallet.json';
import { CloseIcon } from 'components/Images';
import useSWR from 'swr';
import { fetcher } from 'utils/fetcher';
import { Form, Field } from 'react-final-form';
import { FORM_ERROR } from 'final-form';
import { OwnersContext } from 'contexts/OwnersContext';

interface FormValues {
nConfirms: number;
Expand All @@ -24,22 +19,8 @@ interface ConfirmsModalProps {
}

export const ConfirmsModal: FC<ConfirmsModalProps> = ({ address, currentConfirmations }) => {
const { library } = useWeb3React<Web3Provider>();
const { owners, changeRequirement } = useContext(OwnersContext);
const { clearModal } = useContext(ModalContext);
const contract = new Contract(address, multisigAbi);
const {
data: owners,
mutate,
}: {
data?: string[];
mutate: Function;
} = useSWR(library ? [address, 'getOwners'] : null, {
fetcher: fetcher(library, multisigAbi),
});

useEffect(() => {
mutate(undefined, true);
}, []);

const canSubmit = (values: FormValues, errors: FormErrors) => {
const hasValues = values.nConfirms && values.nConfirms !== currentConfirmations;
Expand All @@ -48,18 +29,6 @@ export const ConfirmsModal: FC<ConfirmsModalProps> = ({ address, currentConfirma
return hasValues && !hasErrors;
};

const changeRequirement = async (nConfirms: number) => {
const tx = await contract
.connect(library.getSigner())
.submitTransaction(
contract.address,
0,
contract.interface.encodeFunctionData('changeRequirement', [nConfirms])
);
const receipt = await tx.wait();
return receipt;
};

const sendTx = async ({ nConfirms }: FormValues) => {
try {
const receipt = await changeRequirement(nConfirms);
Expand Down
18 changes: 2 additions & 16 deletions app/components/Connection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, useEffect, useRef } from 'react';
import { truncateAddress } from 'utils/truncate';
import jazzicon from '@metamask/jazzicon';
import Jazzicon from './Jazzicon';

interface ConnectionProps {
active: boolean;
Expand All @@ -15,20 +15,6 @@ export const Connection: FC<ConnectionProps> = ({
activatingConnector,
showConnectionModal,
}) => {
const jazziconRef = useRef<HTMLDivElement>();

useEffect(() => {
if (!account || !jazziconRef.current || jazziconRef.current.childElementCount) return;

const accountjazziconID = account
.slice(2)
.split('')
.map((char) => char.charCodeAt(0))
.reduce((prev, cur) => prev + cur, 0);
const jazziconEl = jazzicon(16, accountjazziconID);
jazziconRef.current?.appendChild(jazziconEl);
}, [account]);

return (
<button
className="flex items-center ml-4 p-1 rounded border-2 border-gray-100"
Expand All @@ -41,7 +27,7 @@ export const Connection: FC<ConnectionProps> = ({
) : (
'Connect Wallet'
)}
<div className="ml-2" ref={jazziconRef} />
<Jazzicon address={account} />
</button>
);
};
22 changes: 22 additions & 0 deletions app/components/Jazzicon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC, useRef, useEffect } from 'react';
import jazzicon from '@metamask/jazzicon';
import { addressToNumber } from 'utils/utils';

interface JazziconProps {
address: string;
}

const Jazzicon: FC<JazziconProps> = ({ address }) => {
const jazziconRef = useRef<HTMLDivElement>();

useEffect(() => {
if (!address || !jazziconRef.current || jazziconRef.current.childElementCount) return;

const accountjazziconID = addressToNumber(address);
const jazziconEl = jazzicon(16, accountjazziconID);
jazziconRef.current?.appendChild(jazziconEl);
}, [address]);

return <div className="ml-2" ref={jazziconRef} />;
};
export default Jazzicon;
16 changes: 10 additions & 6 deletions app/components/MultisigInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { abi } from 'abi/MultiSigWallet.json';
import { ModalContext } from 'components/Modal';
import { ConfirmsModal } from 'components/ConfirmsModal';
import { ClickableAddress } from './ClickableAddress';
import { OwnersContext } from 'contexts/OwnersContext';

export const MultisigInfo = ({ address }) => {
const { isAccountOwner } = useContext(OwnersContext);
const { library, chainId } = useWeb3React<Web3Provider>();
const { setModal } = useContext(ModalContext);
const {
Expand Down Expand Up @@ -64,12 +66,14 @@ export const MultisigInfo = ({ address }) => {
</h2>
<div className="">
{nConfirms} {nConfirms > 1 ? 'signatures' : 'signature'} needed to execute a transaction{' '}
<button
className="text-sm rounded border px-2 border-gray-400 bg-gray-100 text-gray-800"
onClick={showRequirementModal}
>
Change requirement
</button>
{isAccountOwner && (
<button
className="text-sm rounded border px-2 border-gray-400 bg-gray-100 text-gray-800"
onClick={showRequirementModal}
>
Change Threshold
</button>
)}
</div>
</div>
);
Expand Down
103 changes: 21 additions & 82 deletions app/components/OwnerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useContext, FC } from 'react';
import { useContext, FC } from 'react';
import { useWeb3React } from '@web3-react/core';
import { Web3Provider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
Expand All @@ -8,17 +8,14 @@ import { CloseIcon } from 'components/Images';
import { isAddress } from '@ethersproject/address';
import { Form, Field } from 'react-final-form';
import { FORM_ERROR } from 'final-form';
import useSWR from 'swr';
import { fetcher } from 'utils/fetcher';
import { OwnersContext } from 'contexts/OwnersContext';

interface FormValues {
newOwnerAddress: string;
oldOwnerAddress: string;
}

interface FormErrors {
newOwnerAddress?: string;
oldOwnerAddress?: string;
}

interface AddOwnerModalProps {
Expand All @@ -41,62 +38,22 @@ export const ReplaceOwnerModal: FC<ReplaceOwnerModalProps> = ({ address, ownerTo
);

export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerToBeReplaced }) => {
const { library } = useWeb3React<Web3Provider>();
const { owners, addOwner, replaceOwner } = useContext(OwnersContext);
const { clearModal } = useContext(ModalContext);
const contract = new Contract(address, multisigAbi);
const {
data: owners,
mutate,
}: {
data?: string[];
mutate: Function;
} = useSWR(library ? [address, 'getOwners'] : null, {
fetcher: fetcher(library, multisigAbi),
});

useEffect(() => {
mutate(undefined, true);
}, []);

const canSubmit = (values: FormValues, errors: FormErrors) => {
const hasValues =
values.newOwnerAddress.length &&
(addOrReplace === 'replace' ? values.oldOwnerAddress.length : true);
const hasValues = values.newOwnerAddress.length;
const hasErrors = Boolean(Object.keys(errors).length);

return hasValues && !hasErrors;
};

const addOwner = async (owner: string) => {
const tx = await contract
.connect(library.getSigner())
.submitTransaction(
contract.address,
0,
contract.interface.encodeFunctionData('addOwner', [owner])
);
const receipt = await tx.wait();
return receipt;
};

const replaceOwner = async (owner: string, newOwner: string) => {
const tx = await contract
.connect(library.getSigner())
.submitTransaction(
contract.address,
0,
contract.interface.encodeFunctionData('replaceOwner', [owner, newOwner])
);
const receipt = await tx.wait();
return receipt;
};

const sendTx = async ({ newOwnerAddress, oldOwnerAddress }: FormValues) => {
const sendTx = async ({ newOwnerAddress }: FormValues) => {
try {
const receipt =
addOrReplace === 'add'
? await addOwner(newOwnerAddress)
: await replaceOwner(oldOwnerAddress, newOwnerAddress);
: await replaceOwner(ownerToBeReplaced, newOwnerAddress);
clearModal();
return receipt;
} catch (e) {
Expand All @@ -108,6 +65,10 @@ export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerTo
const inputStyle = 'border border-gray-500 w-80 font-mono';
const labelStyle = '';

if (addOrReplace === 'replace' && !addOrReplace.length) {
return null;
}

return (
<Modal>
<div className="flex justify-between w-full bg-gray-200 p-3 font-semibold">
Expand All @@ -121,9 +82,8 @@ export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerTo
onSubmit={sendTx}
initialValues={{
newOwnerAddress: '',
oldOwnerAddress: ownerToBeReplaced ?? '',
}}
validate={({ newOwnerAddress, oldOwnerAddress }: FormValues) => {
validate={({ newOwnerAddress }: FormValues) => {
const errors: FormErrors = {};

if (newOwnerAddress?.length && !isAddress(newOwnerAddress)) {
Expand All @@ -135,17 +95,6 @@ export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerTo
errors.newOwnerAddress = 'This address is already an owner';
}

if (addOrReplace === 'replace') {
if (oldOwnerAddress?.length && !isAddress(oldOwnerAddress)) {
errors.oldOwnerAddress = 'Please enter a valid address';
} else if (
oldOwnerAddress?.length &&
!owners.some((owner) => owner.toUpperCase() === oldOwnerAddress.toUpperCase())
) {
errors.oldOwnerAddress = 'This address is not an owner';
}
}

return errors;
}}
render={({ handleSubmit, errors, submitting, values, submitError }) => (
Expand All @@ -157,6 +106,16 @@ export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerTo
)}
<form className="pb-5" onSubmit={handleSubmit}>
<ul>
{addOrReplace === 'replace' && (
<li>
<div className="flex-col m-4">
<div className="flex-col">
<div>Replace Owner:</div>
<div>{ownerToBeReplaced}</div>
</div>
</div>
</li>
)}
<li>
<Field name="newOwnerAddress" parse={(value) => String(value)}>
{({ input, meta }) => (
Expand All @@ -175,26 +134,6 @@ export const OwnerModal: FC<OwnerModalProps> = ({ address, addOrReplace, ownerTo
)}
</Field>
</li>
{addOrReplace === 'replace' && (
<li>
<Field name="oldOwnerAddress" parse={(value) => String(value)}>
{({ input, meta }) => (
<div className="flex-col m-4">
<div className={itemStyle}>
<label className={labelStyle}>Replaced owner address</label>
<input
{...input}
className={`${inputStyle} ${meta.error ? 'text-red-500' : ''}`}
/>
</div>
{meta.error && (
<div className="text-right text-red-500 m-1">{meta.error}</div>
)}
</div>
)}
</Field>
</li>
)}
</ul>
<button
disabled={!canSubmit(values, errors as FormErrors) || submitting}
Expand Down
Loading