diff --git a/packages/app/src/components/MultiSelectBlock.tsx b/packages/app/src/components/MultiSelectBlock.tsx new file mode 100644 index 00000000..0c9fcd77 --- /dev/null +++ b/packages/app/src/components/MultiSelectBlock.tsx @@ -0,0 +1,121 @@ +import { Grid, makeStyles, Typography } from "@material-ui/core" +import React from "react" +import { colors, ZodiacPaper } from "zodiac-ui-components" +import Creatable from "react-select/creatable" +import { CreatableProps } from "react-select/creatable" +import { GroupBase } from "react-select" + +const components = { + DropdownIndicator: null, +} + +export interface MultiSelectValues { + label: string + value: string +} + +const useStyles = makeStyles((theme) => ({ + paperContainer: { + background: "rgba(0, 0, 0, 0.2)", + }, + message: { + fontSize: 12, + color: "rgba(244, 67, 54, 1)", + }, +})) + +const customStyles = { + control: (base: any, state: { isFocused: any }) => ({ + ...base, + background: "none", + width: "100%", + border: "none", + fontFamily: "Roboto Mono !important", + color: "yellow !important", + boxShadow: state.isFocused ? null : null, + "&:hover": { + border: "none", + }, + }), + option: (base: any) => ({ + ...base, + color: "white", + backgroundColor: "#101010", + cursor: "pointer", + }), + menu: (base: any) => ({ + ...base, + // override border radius to match the box + borderRadius: 0, + backgroundColor: "#101010", + // kill the gap + marginTop: 0, + }), + menuList: (base: any) => ({ + ...base, + // kill the white space on first and last option + padding: 0, + }), + ValueContainer: (base: any) => ({ + ...base, + display: "block", + }), + multiValue: (base: any) => ({ + ...base, + color: "white !important", + background: colors.tan[300], + "&:hover": { + background: colors.tan[300], + }, + "& > div": { + color: `white !important`, + }, + "& > div[role=button]:hover": { + cursor: "pointer", + color: "blue", + background: colors.tan[500], + }, + }), +} + +interface MultiSelectBlockCustomProps + extends CreatableProps> { + invalidText?: string +} + +export const MultiSelectBlock: React.FC = (props) => { + const classes = useStyles() + + return ( + + + + ({ + ...theme, + colors: { + ...theme.colors, + font: "#101010", + primary25: "#101010", + primary: "#101010", + neutral80: "white", + }, + })} + /> + + + {props.invalidText && ( + + {props.invalidText} + + )} + + ) +} diff --git a/packages/app/src/components/ethereum/Address.tsx b/packages/app/src/components/ethereum/Address.tsx index e9b80f07..0d19eee7 100644 --- a/packages/app/src/components/ethereum/Address.tsx +++ b/packages/app/src/components/ethereum/Address.tsx @@ -1,22 +1,22 @@ -import React from "react"; -import { makeStyles, Typography, TypographyProps } from "@material-ui/core"; -import { CopyToClipboardBtn } from "@gnosis.pm/safe-react-components"; -import { AddressExplorerButton } from "./AddressExplorerButton"; -import { shortAddress } from "../../utils/string"; -import classNames from "classnames"; +import React from "react" +import { makeStyles, Typography, TypographyProps } from "@material-ui/core" +import { CopyToClipboardBtn } from "@gnosis.pm/safe-react-components" +import { AddressExplorerButton } from "./AddressExplorerButton" +import { shortAddress } from "../../utils/string" +import classNames from "classnames" interface AddressProps { - address: string; - short?: boolean; - hideCopyBtn?: boolean; - hideExplorerBtn?: boolean; - showOnHover?: boolean; - gutterBottom?: boolean; + address: string + short?: boolean + hideCopyBtn?: boolean + hideExplorerBtn?: boolean + showOnHover?: boolean + gutterBottom?: boolean classes?: { - icon?: string; - container?: string; - }; - TypographyProps?: TypographyProps; + icon?: string + container?: string + } + TypographyProps?: TypographyProps } const useStyles = makeStyles((theme) => ({ @@ -41,7 +41,7 @@ const useStyles = makeStyles((theme) => ({ visibility: "initial", }, }, -})); +})) export const Address = ({ address, @@ -53,7 +53,7 @@ export const Address = ({ classes: { icon, container } = {}, TypographyProps, }: AddressProps) => { - const classes = useStyles(); + const classes = useStyles() return (
{hideCopyBtn ? null : ( - + )} {hideExplorerBtn ? null : ( - + )}
- ); -}; + ) +} diff --git a/packages/app/src/components/ethereum/AddressExplorerButton.tsx b/packages/app/src/components/ethereum/AddressExplorerButton.tsx index c413914c..818748df 100644 --- a/packages/app/src/components/ethereum/AddressExplorerButton.tsx +++ b/packages/app/src/components/ethereum/AddressExplorerButton.tsx @@ -1,19 +1,19 @@ -import React from "react"; -import { ExplorerButton } from "@gnosis.pm/safe-react-components"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { getExplorerInfo } from "../../utils/explorers"; +import React from "react" +import { ExplorerButton } from "@gnosis.pm/safe-react-components" +import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk" +import { getExplorerInfo } from "../../utils/explorers" interface AddressExplorerButtonProps { - className?: string; - address: string; + className?: string + address: string } export const AddressExplorerButton = ({ address, className, }: AddressExplorerButtonProps) => { - const { safe } = useSafeAppsSDK(); - const safeExplorer = getExplorerInfo(safe.chainId, address); - if (!safeExplorer) return null; - return ; -}; + const { safe } = useSafeAppsSDK() + const safeExplorer = getExplorerInfo(safe.chainId, address) + if (!safeExplorer) return null + return +} diff --git a/packages/app/src/components/ethereum/HashInfo.tsx b/packages/app/src/components/ethereum/HashInfo.tsx index 5886dcda..243e8ad7 100644 --- a/packages/app/src/components/ethereum/HashInfo.tsx +++ b/packages/app/src/components/ethereum/HashInfo.tsx @@ -1,6 +1,6 @@ -import { EthHashInfo } from "@gnosis.pm/safe-react-components"; -import React from "react"; -import { makeStyles } from "@material-ui/core"; +import { EthHashInfo } from "@gnosis.pm/safe-react-components" +import React from "react" +import { makeStyles } from "@material-ui/core" const useStyles = makeStyles((theme) => { return { @@ -27,10 +27,10 @@ const useStyles = makeStyles((theme) => { fontSize: 14, }, }, - }; -}); + } +}) export const HashInfo = (props: Parameters[0]) => { - const classes = useStyles(); - return ; -}; + const classes = useStyles() + return +} diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 6078cdc2..86926e79 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -1,20 +1,14 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { ThemeProvider } from "styled-components"; -import { - CssBaseline, - ThemeProvider as MUIThemeProvider, -} from "@material-ui/core"; -import { Loader } from "@gnosis.pm/safe-react-components"; -import SafeProvider from "@gnosis.pm/safe-apps-react-sdk"; -import App from "./App"; -import { Provider } from "react-redux"; -import { REDUX_STORE } from "./store"; -import { Row } from "./components/layout/Row"; -import { - zodiacMuiTheme, - gnosisStyledComponentsTheme, -} from "zodiac-ui-components"; +import React from "react" +import ReactDOM from "react-dom" +import { ThemeProvider } from "styled-components" +import { CssBaseline, ThemeProvider as MUIThemeProvider } from "@material-ui/core" +import { Loader } from "@gnosis.pm/safe-react-components" +import SafeProvider from "@gnosis.pm/safe-apps-react-sdk" +import App from "./App" +import { Provider } from "react-redux" +import { REDUX_STORE } from "./store" +import { Row } from "./components/layout/Row" +import { zodiacMuiTheme, gnosisStyledComponentsTheme } from "zodiac-ui-components" const Main = () => { return ( @@ -40,12 +34,12 @@ const Main = () => { - ); -}; + ) +} ReactDOM.render(
, - document.getElementById("root") -); + document.getElementById("root"), +) diff --git a/packages/app/src/services/ens.ts b/packages/app/src/services/ens.ts index cd6a7399..d238381c 100644 --- a/packages/app/src/services/ens.ts +++ b/packages/app/src/services/ens.ts @@ -82,7 +82,11 @@ export const getEnsTextRecord = async ( const nameHash = ethers.utils.namehash(ensName) const ensRegistryContract = new ethers.Contract(ensRegistry, abiRegistry, provider) const ensResolverAddress = await ensRegistryContract.resolver(nameHash) - const ensResolverContract = new ethers.Contract(ensResolverAddress, abiPublicResolver, provider) + const ensResolverContract = new ethers.Contract( + ensResolverAddress, + abiPublicResolver, + provider, + ) const record = ensResolverContract.functions.text(nameHash, recordId) return record } diff --git a/packages/app/src/services/index.ts b/packages/app/src/services/index.ts index c15130cc..bb042313 100644 --- a/packages/app/src/services/index.ts +++ b/packages/app/src/services/index.ts @@ -57,6 +57,10 @@ export interface RolesModifierParams { target: string multisend: string } +export interface RolesV2ModifierParams { + target: string + multisend: string[] +} export interface AMBModuleParams { amb: string @@ -610,7 +614,7 @@ export function deployRolesV2Modifier( provider: JsonRpcProvider, safeAddress: string, chainId: number, - args: RolesModifierParams, + args: RolesV2ModifierParams, ) { const { target, multisend } = args const { transaction: deployAndSetupTx, expectedModuleAddress: expectedRolesAddress } = @@ -634,15 +638,15 @@ export function deployRolesV2Modifier( const MULTISEND_SELECTOR = "0x8d80ff0a" const MULTISEND_UNWRAPPER = "0x93B7fCbc63ED8a3a24B59e1C3e6649D50B7427c0" - const setUnwrapperTx = { + const setUnwrapperTxs = multisend.map((address) => ({ to: rolesContract.address, data: rolesContract.interface.encodeFunctionData("setTransactionUnwrapper", [ - multisend, + address, MULTISEND_SELECTOR, MULTISEND_UNWRAPPER, ]), value: "0", - } + })) return [ { @@ -650,7 +654,7 @@ export function deployRolesV2Modifier( value: deployAndSetupTx.value.toHexString(), }, enableModuleTx, - setUnwrapperTx, + ...setUnwrapperTxs, ] } diff --git a/packages/app/src/views/AddModule/wizards/OzGovernor/sections/Review/index.tsx b/packages/app/src/views/AddModule/wizards/OzGovernor/sections/Review/index.tsx index 7e59aa3e..3f5cf43f 100644 --- a/packages/app/src/views/AddModule/wizards/OzGovernor/sections/Review/index.tsx +++ b/packages/app/src/views/AddModule/wizards/OzGovernor/sections/Review/index.tsx @@ -120,9 +120,9 @@ export const OZReviewSection: React.FC = ({ Voting Token: {token.tokenAddress} diff --git a/packages/app/src/views/AddModule/wizards/RolesModifier/RolesV2ModifierModal.tsx b/packages/app/src/views/AddModule/wizards/RolesModifier/RolesV2ModifierModal.tsx index 1c4130c9..0df2a48a 100644 --- a/packages/app/src/views/AddModule/wizards/RolesModifier/RolesV2ModifierModal.tsx +++ b/packages/app/src/views/AddModule/wizards/RolesModifier/RolesV2ModifierModal.tsx @@ -1,15 +1,15 @@ import React, { useState } from "react" -import { Grid, makeStyles, Typography } from "@material-ui/core" +import { Grid, InputLabel, makeStyles, Typography } from "@material-ui/core" import { AddModuleModal } from "../components/AddModuleModal" -import { deployRolesV2Modifier, RolesModifierParams } from "services" +import { deployRolesV2Modifier, RolesV2ModifierParams } from "services" import { ParamInput } from "../../../../components/ethereum/ParamInput" import { ParamType } from "@ethersproject/abi" import useSafeAppsSDKWithProvider from "hooks/useSafeAppsSDKWithProvider" -import { SafeInfo } from "@gnosis.pm/safe-apps-sdk" import { - networkAddresses as multisendNetworkAddresses, - defaultAddress as defaultMultisendAddress, -} from "@gnosis.pm/safe-deployments/dist/assets/v1.3.0/multi_send.json" + MultiSelectBlock, + MultiSelectValues, +} from "../../../../components/MultiSelectBlock" +import { getAddress } from "ethers/lib/utils" interface RolesModifierModalProps { open: boolean @@ -26,8 +26,15 @@ const useStyles = makeStyles((theme) => ({ loadMessage: { textAlign: "center", }, + multiSendLabel: { + color: theme.palette.text.primary, + marginBottom: 4, + }, })) +const MULTISEND_141 = "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" // used in latest Safes +const MULTISEND_CALLONLY_141 = "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" // used in latest Safes + export const RolesV2ModifierModal = ({ open, onClose, @@ -37,23 +44,25 @@ export const RolesV2ModifierModal = ({ const { sdk, safe, provider } = useSafeAppsSDKWithProvider() - const [errors, setErrors] = useState>({ + const [fieldsValid, setFieldsValid] = useState< + Record + >({ target: true, multisend: true, }) - const [params, setParams] = useState({ + const [params, setParams] = useState({ target: safe.safeAddress, - multisend: defaultMultisend(safe), + multisend: [MULTISEND_141, MULTISEND_CALLONLY_141], }) - const isValid = Object.values(errors).every((field) => field) + const isValid = Object.values(fieldsValid).every((field) => field) - const onParamChange = ( + const onParamChange = ( field: Field, - value: RolesModifierParams[Field], + value: RolesV2ModifierParams[Field], valid: boolean, ) => { - setErrors({ ...errors, [field]: valid }) + setFieldsValid({ ...fieldsValid, [field]: valid }) setParams({ ...params, [field]: value, @@ -98,12 +107,25 @@ export const RolesV2ModifierModal = ({ /> - onParamChange("multisend", value, valid)} + MultiSend Addresses + { + const values = (items as MultiSelectValues[]).map((v) => v.value) + onParamChange("multisend", values, values.every(isValidAddress)) + }} + value={params.multisend.map((address) => ({ + value: address, + label: label(address), + }))} + noOptionsMessage={() => "Paste an address and press enter..."} + formatCreateLabel={(inputValue) => + isValidAddress(inputValue) + ? `Add "${inputValue}"` + : `Invalid address: ${inputValue}` + } /> @@ -111,8 +133,21 @@ export const RolesV2ModifierModal = ({ ) } -function defaultMultisend(safeInfo: SafeInfo) { - const address = (multisendNetworkAddresses as Record)[safeInfo.chainId] +const isValidAddress = (address: string) => { + try { + getAddress(address) + return true + } catch { + return false + } +} - return address || defaultMultisendAddress +const label = (address: string) => { + if (address === MULTISEND_141) { + return address + " (MultiSend v1.4.1)" + } else if (address === MULTISEND_CALLONLY_141) { + return address + " (MultiSendCallOnly v1.4.1)" + } else { + return address + } } diff --git a/packages/app/src/views/AddModule/wizards/components/AddModuleModal.tsx b/packages/app/src/views/AddModule/wizards/components/AddModuleModal.tsx index 9949dd6f..55ef7dc3 100644 --- a/packages/app/src/views/AddModule/wizards/components/AddModuleModal.tsx +++ b/packages/app/src/views/AddModule/wizards/components/AddModuleModal.tsx @@ -1,5 +1,11 @@ import React from "react" -import { ButtonProps as MuiButtonProps, Fade, makeStyles, Modal, Typography } from "@material-ui/core" +import { + ButtonProps as MuiButtonProps, + Fade, + makeStyles, + Modal, + Typography, +} from "@material-ui/core" import { BadgeIcon, ZodiacPaper } from "zodiac-ui-components" import { BadgeIconProps } from "zodiac-ui-components/lib/components/Icons/BadgeIcon/BadgeIcon" import { ActionButton } from "../../../../components/ActionButton" @@ -29,7 +35,7 @@ const useStyles = makeStyles((theme) => ({ root: { width: "100%", outline: "none", - maxWidth: 380, + maxWidth: 525, margin: theme.spacing(14, 1, 1, 1), padding: theme.spacing(2), backgroundColor: "rgba(78, 72, 87, 0.8)", @@ -142,10 +148,19 @@ export const AddModuleModal: React.FC = ({ - {children ?
{children}
: null} + {children ? ( +
+ {children} +
+ ) : null} {hideButton ? null : ( - } onClick={onAdd} {...ButtonProps}> + } + onClick={onAdd} + {...ButtonProps} + > Add Module )} diff --git a/packages/app/src/views/ModuleDetails/contract/ContractFunctionHeader.tsx b/packages/app/src/views/ModuleDetails/contract/ContractFunctionHeader.tsx index 5dbe0a8d..e680211e 100644 --- a/packages/app/src/views/ModuleDetails/contract/ContractFunctionHeader.tsx +++ b/packages/app/src/views/ModuleDetails/contract/ContractFunctionHeader.tsx @@ -1,20 +1,20 @@ -import React from "react"; -import { Address } from "../../../components/ethereum/Address"; -import { makeStyles, Typography } from "@material-ui/core"; -import TimeAgo from "timeago-react"; -import { Skeleton } from "@material-ui/lab"; -import { FunctionFragment } from "@ethersproject/abi"; -import { FunctionOutputs } from "../../../hooks/useContractQuery"; -import { CopyToClipboardBtn } from "@gnosis.pm/safe-react-components"; -import { formatValue } from "../../../utils/contracts"; -import classNames from "classnames"; +import React from "react" +import { Address } from "../../../components/ethereum/Address" +import { makeStyles, Typography } from "@material-ui/core" +import TimeAgo from "timeago-react" +import { Skeleton } from "@material-ui/lab" +import { FunctionFragment } from "@ethersproject/abi" +import { FunctionOutputs } from "../../../hooks/useContractQuery" +import { CopyToClipboardBtn } from "@gnosis.pm/safe-react-components" +import { formatValue } from "../../../utils/contracts" +import classNames from "classnames" interface ContractFunctionHeaderProps { - date?: Date; - func: FunctionFragment; - loading?: boolean; - showResult?: boolean; - result?: FunctionOutputs; + date?: Date + func: FunctionFragment + loading?: boolean + showResult?: boolean + result?: FunctionOutputs } const useStyles = makeStyles((theme) => ({ @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({ queryType: { fontSize: ".75rem", }, -})); +})) export const ContractFunctionHeader = ({ date, @@ -37,38 +37,32 @@ export const ContractFunctionHeader = ({ showResult, loading = false, }: ContractFunctionHeaderProps) => { - const classes = useStyles(); + const classes = useStyles() if (loading) { - return ; + return } if (showResult && result && result.length && func.outputs) { - const { baseType, type } = func.outputs[0]; - const value = formatValue(baseType, result[0]); + const { baseType, type } = func.outputs[0] + const value = formatValue(baseType, result[0]) if (baseType === "address") { return ( -
- ); +
+ ) } return ( <> ({type}) - + {value} - ); + ) } if (date) { @@ -76,8 +70,8 @@ export const ContractFunctionHeader = ({ Queried - ); + ) } - return Query; -}; + return Query +} diff --git a/packages/app/src/views/Panel/item/ModulePendingItem.tsx b/packages/app/src/views/Panel/item/ModulePendingItem.tsx index d5a26b1e..8a4162af 100644 --- a/packages/app/src/views/Panel/item/ModulePendingItem.tsx +++ b/packages/app/src/views/Panel/item/ModulePendingItem.tsx @@ -1,13 +1,13 @@ -import React from "react"; -import { PanelItem, PanelItemProps } from "./PanelItem"; -import { makeStyles, Typography } from "@material-ui/core"; -import { Link } from "../../../components/text/Link"; -import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk"; -import { getNetworkExplorerInfo } from "../../../utils/explorers"; +import React from "react" +import { PanelItem, PanelItemProps } from "./PanelItem" +import { makeStyles, Typography } from "@material-ui/core" +import { Link } from "../../../components/text/Link" +import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk" +import { getNetworkExplorerInfo } from "../../../utils/explorers" interface ModulePendingItemProps extends PanelItemProps { - title: string; - linkText: string; + title: string + linkText: string } const useStyles = makeStyles((theme) => ({ @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({ borderColor: "rgba(255, 255, 255, 0.2)", background: "rgba(224, 197, 173, 0.1)", }, -})); +})) export const ModulePendingItem = ({ image = null, @@ -36,13 +36,11 @@ export const ModulePendingItem = ({ title, ...props }: ModulePendingItemProps) => { - const classes = useStyles(); - const { safe } = useSafeAppsSDK(); + const classes = useStyles() + const { safe } = useSafeAppsSDK() - const network = getNetworkExplorerInfo(safe.chainId); - const link = network - ? `${network.safeUrl}${safe.safeAddress}/transactions` - : ""; + const network = getNetworkExplorerInfo(safe.chainId) + const link = network ? `${network.safeUrl}${safe.safeAddress}/transactions` : "" return ( {image}} {...props}> @@ -55,5 +53,5 @@ export const ModulePendingItem = ({ - ); -}; + ) +} diff --git a/packages/app/src/views/TransactionBuilder/components/TransactionBlockHeaderButtons.tsx b/packages/app/src/views/TransactionBuilder/components/TransactionBlockHeaderButtons.tsx index a231a1d4..0780ac7f 100644 --- a/packages/app/src/views/TransactionBuilder/components/TransactionBlockHeaderButtons.tsx +++ b/packages/app/src/views/TransactionBuilder/components/TransactionBlockHeaderButtons.tsx @@ -1,15 +1,15 @@ -import React from "react"; -import { Box, makeStyles } from "@material-ui/core"; -import { Icon } from "@gnosis.pm/safe-react-components"; -import { ActionButton } from "../../../components/ActionButton"; +import React from "react" +import { Box, makeStyles } from "@material-ui/core" +import { Icon } from "@gnosis.pm/safe-react-components" +import { ActionButton } from "../../../components/ActionButton" export interface TransactionBlockHeaderButtonsProps { - edit?: boolean; - disabled?: boolean; - onEdit?(): void; - onDelete?(): void; - onSave?(): void; - onCancel?(): void; + edit?: boolean + disabled?: boolean + onEdit?(): void + onDelete?(): void + onSave?(): void + onCancel?(): void } const useStyles = makeStyles((theme) => ({ @@ -31,7 +31,7 @@ const useStyles = makeStyles((theme) => ({ label: { color: theme.palette.text.primary, }, -})); +})) export const TransactionBlockHeaderButtons = ({ edit = false, @@ -41,16 +41,12 @@ export const TransactionBlockHeaderButtons = ({ onSave, onDelete, }: TransactionBlockHeaderButtonsProps) => { - const classes = useStyles(); + const classes = useStyles() if (edit) { return ( <> - + Save Changes - ); + ) } return ( @@ -74,5 +70,5 @@ export const TransactionBlockHeaderButtons = ({ - ); -}; + ) +}