diff --git a/.vscode/settings.json b/.vscode/settings.json index f6c088de..903520d7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,4 @@ "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } -} \ No newline at end of file +} diff --git a/README.md b/README.md index dc06ad1c..db49934e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The following dependencies are required to run homebase-app: | Dependency | Version | | ---------- | ------------------- | -| Node | `v16.16.0` or above | +| Node | `v18.20.0` or above | | Yarn | `v1.22.*` or above | # Third Party Services diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 8cc4c015..00000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package.json b/package.json index 7fd883c8..aebf4f0a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/valid-url": "^1.0.4", "@wagmi/core": "2.13.4", "assert": "^2.0.0", + "assert-never": "^1.2.1", "bignumber.js": "^9.0.1", "blockies-ts": "^1.0.0", "caniuse-lite": "", @@ -95,8 +96,8 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/jest": "^29.5.12", "@types/node": "^14.14.35", - "@types/react": "^17.0.3", - "@types/react-dom": "^17.0.2", + "@types/react": "^17.0.44", + "@types/react-dom": "^18.2.25", "@types/react-html-parser": "^2.0.2", "@types/react-router-dom": "^5.1.6", "@types/yup": "^0.29.11", diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 00000000..92d1e735 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,28 @@ +import { styled, Button as MaterialButton } from "@material-ui/core" + +export const Button = styled(MaterialButton)(({ theme }) => ({ + "fontSize": "14px", + "justifyItems": "center", + "color": "#000", + "boxShadow": "0px 0px 7px -2px rgba(0, 0, 0, 0.2)", + "transition": ".15s ease-in", + "background": theme.palette.secondary.main, + "textTransform": "none", + "borderRadius": 4, + "padding": "8px 15px", + "marginRight": "8px", + + "&$disabled": { + boxShadow: "none" + }, + + "&:hover": { + boxShadow: "0px 0px 7px -2px rgba(0, 0, 0, 0.2)", + backgroundColor: "#62eda5 !important", + transition: ".15s ease-in" + }, + + ["@media (max-width:1030px)"]: { + fontSize: "14px" + } +})) diff --git a/src/components/ui/ConfigProposalForm.tsx b/src/components/ui/ConfigProposalForm.tsx new file mode 100644 index 00000000..70792371 --- /dev/null +++ b/src/components/ui/ConfigProposalForm.tsx @@ -0,0 +1,20 @@ +import { Grid, Typography, styled, CircularProgress } from "@material-ui/core" +import { CheckOutlined } from "@material-ui/icons" + +const StyledRow = styled(Grid)({ + marginTop: 30 +}) + +const LoadingContainer = styled(Grid)({ + minHeight: 651 +}) + +const LoadingStateLabel = styled(Typography)({ + marginTop: 40 +}) + +const CheckIcon = styled(CheckOutlined)({ + fontSize: 169 +}) + +export { StyledRow, LoadingContainer, LoadingStateLabel, CheckIcon } diff --git a/src/components/ui/Table.tsx b/src/components/ui/Table.tsx new file mode 100644 index 00000000..e868ac9a --- /dev/null +++ b/src/components/ui/Table.tsx @@ -0,0 +1,18 @@ +import { styled, Grid, Theme } from "@material-ui/core" + +export const ContentContainer = styled(Grid)(({ theme }) => ({ + borderRadius: 8, + background: "#24282D" +})) + +export const TableHeader = styled(Grid)(({ theme }: { theme: Theme }) => ({ + padding: "16px 46px", + minHeight: 34, + [theme.breakpoints.down("sm")]: { + gap: 10 + } +})) + +export const TableContainer = styled(ContentContainer)({ + width: "100%" +}) diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..0a28bb7e --- /dev/null +++ b/src/config.ts @@ -0,0 +1,15 @@ +const AppConfig = { + env: process.env.REACT_APP_ENV, + CONST: { + ARBITRARY_CONTRACT_INTERACTION: "arbitrary_contract_interaction" + }, + ACI: { + EXECUTOR_FUNCTION_NAME: "aci_executor", + EXECUTOR_LAMBDA: { + code: `(Left (Left (Pair (Pair { UNPAIR; UNPAIR; SWAP; UNPACK (pair (lambda %code (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) (bytes %packed_argument)); ASSERT_SOME; UNPAIR; DIP{ SWAP; PAIR; PAIR}; SWAP; EXEC} {DROP; UNIT}) "aci_executor")))`, + type: `(or (or (pair %add_handler (pair (lambda %code (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) (lambda %handler_check (pair bytes (map string bytes)) unit)) (string %name)) (pair %execute_handler (string %handler_name) (bytes %packed_argument))) (string %remove_handler))` + } + } +} + +export default AppConfig diff --git a/src/models/Contract.ts b/src/models/Contract.ts new file mode 100644 index 00000000..b5900547 --- /dev/null +++ b/src/models/Contract.ts @@ -0,0 +1,26 @@ +export interface ArbitraryContract { + counter: number + name: string + type: string + children: ContractChild[] +} + +interface ContractChild { + counter: number + name: string + type: string + children: ParametersList[] + placeholder: string + validate: any + initValue: string +} + +interface ParametersList { + counter: number + name: string + type: string + children: ParametersList[] + placeholder: string + validate: any + initValue: string +} diff --git a/src/modules/common/SmallButton.tsx b/src/modules/common/SmallButton.tsx index df190a0f..228476b5 100644 --- a/src/modules/common/SmallButton.tsx +++ b/src/modules/common/SmallButton.tsx @@ -21,3 +21,27 @@ export const SmallButton = styled(Button)({ transition: ".15s ease-in" } }) + +export const SmallButtonDialog = styled(Button)({ + "justifyItems": "center", + "fontSize": "16px", + "boxShadow": "0px 0px 7px -2px rgba(0, 0, 0, 0.2)", + "transition": ".15s ease-out", + "textTransform": "capitalize", + "borderRadius": 8, + "backgroundColor": "#81feb7 !important", + "color": "#1c1f23", + "padding": "8px 16px", + + "&$disabled": { + boxShadow: "none", + backgroundColor: "#474E55 !important", + border: "none" + }, + + "&:hover": { + boxShadow: "0px 0px 7px -2px rgba(0, 0, 0, 0.2)", + backgroundColor: "#62eda5 !important", + transition: ".15s ease-in" + } +}) diff --git a/src/modules/common/StyledSendButton.tsx b/src/modules/common/StyledSendButton.tsx new file mode 100644 index 00000000..e0d2847e --- /dev/null +++ b/src/modules/common/StyledSendButton.tsx @@ -0,0 +1,14 @@ +import { styled } from "@material-ui/core" +import { MainButton } from "./MainButton" + +const StyledSendButton = styled(MainButton)(({ theme }) => ({ + "width": 101, + "color": "#1C1F23", + "&$disabled": { + opacity: 0.5, + boxShadow: "none", + cursor: "not-allowed" + } +})) + +export { StyledSendButton } diff --git a/src/modules/explorer/components/AllProposalsList.tsx b/src/modules/explorer/components/AllProposalsList.tsx index b817388c..3fb701bd 100644 --- a/src/modules/explorer/components/AllProposalsList.tsx +++ b/src/modules/explorer/components/AllProposalsList.tsx @@ -3,20 +3,8 @@ import { ProposalItem } from "modules/explorer/pages/User" import React, { useCallback, useEffect, useState } from "react" import { Link } from "react-router-dom" import { Proposal, ProposalStatus } from "services/services/dao/mappers/proposal/types" -import { ContentContainer } from "./ContentContainer" import { ProposalFilter } from "./ProposalsFilter" - -const TableContainer = styled(ContentContainer)({ - width: "100%" -}) - -const TableHeader = styled(Grid)(({ theme }: { theme: Theme }) => ({ - padding: "16px 46px", - minHeight: 34, - [theme.breakpoints.down("sm")]: { - gap: 10 - } -})) +import { TableContainer, TableHeader } from "components/ui/Table" const ProposalsFooter = styled(Grid)({ padding: "16px 46px", diff --git a/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx new file mode 100644 index 00000000..1c032f8b --- /dev/null +++ b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx @@ -0,0 +1,641 @@ +import React, { useEffect, useMemo, useState } from "react" +import { + CircularProgress, + Grid, + InputAdornment, + Paper, + TextField, + Typography, + styled, + useMediaQuery, + useTheme, + withStyles +} from "@material-ui/core" +import { ProposalFormInput } from "./ProposalFormInput" +import { validateContractAddress } from "@taquito/utils" +import { Field, FieldArray, Form, Formik, FormikErrors, getIn } from "formik" +import { SmallButtonDialog } from "modules/common/SmallButton" +import { ArrowBackIos } from "@material-ui/icons" +import { ContractEndpoint, SearchEndpoints } from "./SearchEndpoints" +import { formatUnits, toShortAddress } from "services/contracts/utils" +import { useArbitraryContractData } from "services/aci/useArbitratyContractData" +import { useTezos } from "services/beacon/hooks/useTezos" +import { ArbitraryContract } from "models/Contract" +import { evalTaquitoParam, generateExecuteContractMichelson } from "services/aci" +import { emitMicheline, Parser } from "@taquito/michel-codec" +import ProposalExecuteForm from "./ProposalExecuteForm" +import { Expr, MichelsonData } from "@taquito/michel-codec" +import { getContract } from "services/contracts/baseDAO" +import { TezosToolkit } from "@taquito/taquito" +import { useDAO } from "services/services/dao/hooks/useDAO" +import { useDAOID } from "../pages/DAO/router" +import AppConfig from "config" +import { Link } from "react-router-dom" +import { Button } from "components/ui/Button" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { useNotification } from "modules/common/hooks/useNotification" + +// Base ACI Lambda +const aciBaseLambda = { + code: `Left (Left (Pair (Pair { UNPAIR; UNPAIR; SWAP; UNPACK (pair (lambda %code (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) (bytes %packed_argument)); ASSERT_SOME; UNPAIR; DIP{ SWAP; PAIR; PAIR}; SWAP; EXEC} {DROP; UNIT}) "simple_lambda"))`, + type: `(or (or (pair %add_handler (pair (lambda %code (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) (lambda %handler_check (pair bytes (map string bytes)) unit)) (string %name)) (pair %execute_handler (string %handler_name) (bytes %packed_argument))) (string %remove_handler))` +} + +const aciLambda = { + // code: 'Pair {NIL operation; PUSH address "KT1T17GC91HrJ8ijZgnMaE9j4PZbojbVAn73"; CONTRACT %change_string string; ASSERT_SOME ;PUSH mutez 0;PUSH string "new string"; TRANSFER_TOKENS; CONS; SWAP; CAR; CAR; NONE address; PAIR; PAIR} 0x', + type: `pair (lambda (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) bytes` +} + +const executionLambda = { + code: (hash: string, executionLambdaName = "aci_executor") => + `(Left (Right (Pair "${executionLambdaName}" 0x${hash})))`, + type: "(or (or (pair %add_handler (pair (lambda %code (pair (pair (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) (lambda %handler_check (pair bytes (map string bytes)) unit)) (string %name)) (pair %execute_handler (string %handler_name) (bytes %packed_argument))) (string %remove_handler))" +} + +async function packLambda(tezos: TezosToolkit, lambdaCode: string, lambdaType: string): Promise { + console.log("PACKLAMBDA", lambdaCode, lambdaType) + const parser = new Parser() + const michelsonData = lambdaCode + const mData = parser.parseData(michelsonData) + const michelsonType = parser.parseData(lambdaType) + const { packed } = await tezos.rpc.packData({ + data: mData as unknown as MichelsonData, + type: michelsonType as unknown as Expr + }) + return packed +} + +async function prepareContractData(tezos: TezosToolkit, lambdaCode: string, lambdaType: string): Promise { + console.log("prepareContractData", { lambdaCode }) + + /** + * This needs to be deployed to the DAO + * e.g, https://better-call.dev/ghostnet/KT1VG3ynsnyxFGzw9mdBwYnyZAF8HZvqnNkw/storage/big_map/336003/keys + * + * If not, we need to create a proposal to just deploy it first. + * */ + // const packedBaseAci = await packLambda(tezos, aciBaseLambda.code, aciBaseLambda.type) + // console.log("ACILambdaCode", lambdaCode, packedBaseAci) + + const packedLambdaBytes = await packLambda(tezos, `Pair ${lambdaCode} 0x`, lambdaType) + const execLambdaCode = executionLambda.code(packedLambdaBytes, "aci_executor") + const execLambdaType = executionLambda.type + const finalPackedDataBytes = await packLambda(tezos, execLambdaCode, execLambdaType) + + // const finalPackedData = await packLambda(tezos, executionLambda.code(packedLambda, "aci_executor"), executionLambda.type) + + // const contract = await getContract(tezos, daoAddress) + + // TODO: Replace with actual frozn token value + + // const frozenToken = formatUnits(daoDetails?.extra?.frozen_extra_value, daoDetails?.token?.decimals) + // const contractMethod = contract.methods.propose(await tezos.wallet.pkh(), frozenToken, finalPackedDataBytes) + + // const result = await contractMethod.send() + // console.log("RESULT", result) + + return finalPackedDataBytes + + // return contractMethod.send() +} + +interface Parameter { + key: string + type: string + value?: any +} + +const TypeText = styled(Typography)(({ theme }) => ({ + fontSize: 14, + fontWeight: 300, + color: theme.palette.primary.light +})) + +const Container = styled(`div`)({ + display: "inline-grid", + gap: 32 +}) + +const BackButton = styled(Paper)({ + cursor: "pointer", + background: "inherit", + width: "fit-content", + display: "flex", + boxShadow: "none", + alignItems: "center" +}) + +const ErrorText = styled(Typography)({ + fontSize: 14, + color: "red", + marginTop: 4 +}) + +const Title = styled(Typography)({ + fontSize: 18, + fontWeight: 450 +}) + +const Value = styled(Typography)(({ theme }) => ({ + fontSize: 18, + fontWeight: 300, + lineHeight: "160%", + color: theme.palette.primary.light +})) + +const SubContainer = styled(Grid)({ + gap: 8, + display: "inline-grid" +}) + +const BackButtonIcon = styled(ArrowBackIos)(({ theme }) => ({ + color: theme.palette.secondary.main, + fontSize: 12, + marginRight: 16, + cursor: "pointer" +})) + +type ACIValues = { + destination_contract: any + destination_contract_address: string + amount: number + target_endpoint: string + parameters: Parameter[] +} + +enum Status { + NEW_INTERACTION = 0, + CONTRACT_VALIDATED = 1, + ENDPOINT_SELECTED = 2 +} + +const CustomFormikTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + } + } +})(TextField) + +const ContractInteractionForm = ({ + submitForm, + values, + setFieldValue, + errors, + touched, + setFieldTouched, + setFieldError, + isValid, + showHeader, + daoLambdas +}: any) => { + const daoId = useDAOID() + const nofity = useNotification() + const [state, setState] = useState(Status.NEW_INTERACTION) + const [formState, setFormState] = useState({ address: "", amount: 0, shape: {} }) + const [endpoint, setEndpoint] = useState(undefined) + const theme = useTheme() + const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) + const { mutate: fetchContractData, data } = useArbitraryContractData() + const isAciDeployerDeployed = daoLambdas?.find((lambda: any) => lambda.key === AppConfig.ACI.EXECUTOR_FUNCTION_NAME) + + const { tezos, network } = useTezos() + const [isLoading, setIsLoading] = useState(false) + const { data: daoDetails } = useDAO(daoId) + + const shouldContinue = useMemo(() => { + if (values.destination_contract_address !== "" && !errors.destination_contract_address) { + return false + } + return true + }, [values, errors]) + + const validateAddress = () => { + if (getIn(values, "amount") === "") { + setFieldValue("amount", 0) + } + setIsLoading(true) + fetchContractData({ + contract: getIn(values, "destination_contract_address"), + network: network, + handleContinue: () => setState(Status.CONTRACT_VALIDATED), + finishLoad: () => setIsLoading(false), + showHeader: () => showHeader(false) + }) + } + + const processParameters = (data: ContractEndpoint) => { + setEndpoint(data) + setFieldValue("parameters", data.params) + setFieldValue("target_endpoint", data.name) + } + + const goBack = () => { + showHeader(true) + setState(Status.NEW_INTERACTION) + setEndpoint(undefined) + } + + if (!isAciDeployerDeployed && !isLoading) { + return ( +
+ We need to deploy the ACI Deployer Contract + + Deploy ACI Deployer Contract + +
+ ) + } + + return ( + <> + {state === Status.NEW_INTERACTION ? ( + + + + setFieldTouched("destination_contract_address")} + onChange={(newValue: any) => { + const contractAddress = newValue.target.value.trim() + console.log("Destination Contract Address", contractAddress) + setFieldValue("destination_contract_address", contractAddress) + + if (validateContractAddress(contractAddress) === 3) { + tezos.contract.at(contractAddress).then((contract: any) => { + setFieldValue("destination_contract", contract) + }) + } else { + console.log("invalid address", contractAddress) + } + }} + value={getIn(values, "destination_contract_address")} + inputProps={{ + maxLength: 36 + }} + /> + + {errors.destination_contract_address && touched.destination_contract_address ? ( + {errors.destination_contract_address} + ) : null} + + + + setFieldTouched("amount")} + onChange={(newValue: any) => { + setFieldValue("amount", newValue.target.value) + }} + value={getIn(values, "amount")} + InputProps={{ + endAdornment: ( + + XTZ + + ) + }} + /> + + + + ) : state === Status.CONTRACT_VALIDATED ? ( + <> + + + Calling Contract + + {isMobileSmall + ? toShortAddress(getIn(values, "destination_contract_address")) + : getIn(values, "destination_contract_address")} + + + + With an attached value of + {getIn(values, "amount")} XTZ + + + Contract Endpoint + setFormState({ address: "", amount: 0, shape: {} })} + setField={(lambdaCode: string, metadata: string) => { + // debugger + // console.log("SetField", lambdaCode, metadata, values.destination_contract_address) + // prepareContractData(tezos, lambdaCode, aciLambda.type).then((packedBytes: string) => { + // console.log("Packed LambdaX", packedBytes) + // }) + }} + setLoading={() => {}} + setState={shape => { + // debugger + console.log("New Shape", shape) + setFormState((v: any) => ({ ...v, shape })) + }} + onReset={() => { + setFormState({ address: "", amount: 0, shape: {} }) + // props.onReset() + }} + loading={false} + onShapeChange={shapeInitValue => { + setFormState((v: any) => ({ + ...v, + shape: { ...v?.shape, ...shapeInitValue } + })) + }} + /> + + + {/* ACI: Endpoint list */} + {endpoint && ( + + ( + + {endpoint.params.length > 0 && + endpoint.params.map((param, index) => ( +
+ + { + setFieldValue(`parameters.${index}.value`, newValue.target.value, false) + if (newValue.target.value === "") { + setFieldError(`parameters.${index}`, "Required") + } + }} + InputProps={{ + endAdornment: ( + + {`( ${param.type} )`} + + ) + }} + /> + + {errors.parameters && errors.parameters[index] ? ( + {errors.parameters[index]} + ) : null} +
+ ))} +
+ )} + /> +
+ )} +
+ + ) : null} + + {state === Status.NEW_INTERACTION ? ( + + {isLoading ? ( + + ) : ( + + Continue + + )} + + ) : state === Status.CONTRACT_VALIDATED ? ( + + + + + Back + + + + { + console.log({ formState }) + + try { + let entrypoint = formState.shape.token.initValue // accept_ownership | default etc + let taquitoParam + + setIsLoading(true) + + const execContract = formState.shape.contract + const taquitoFullParam = evalTaquitoParam(formState.shape.token, formState.shape) + if (execContract?.parameterSchema.isMultipleEntryPoint) { + const p = Object.entries(taquitoFullParam) + if (p.length !== 1) { + throw new Error("should only one entrypoint is selected") + } + ;[entrypoint, taquitoParam] = p[0] + } else { + taquitoParam = taquitoFullParam + } + const param = emitMicheline( + execContract?.methodsObject[entrypoint](taquitoParam).toTransferParams()?.parameter?.value + ) + + const micheline_type = execContract?.parameterSchema.isMultipleEntryPoint + ? execContract?.entrypoints.entrypoints[entrypoint] + : execContract?.parameterSchema.root.val + + const p = new Parser() + const type = emitMicheline(p.parseJSON(micheline_type), { + indent: "", + newline: "" + }) + + console.log("Lambda Param", param) + const lambda = generateExecuteContractMichelson("1.0.0", { + address: values.destination_contract_address, + entrypoint, + type, + amount: values.amount, + param + }) + + const finalPackedDataBytes = await prepareContractData(tezos, lambda, aciLambda.type) + const contract = await getContract(tezos, daoDetails?.data?.address as string) + + console.log("DaoTokenDetails", daoDetails?.data?.token) + console.log( + "Frozen Token Params", + daoDetails?.data?.extra?.frozen_extra_value, + daoDetails?.data?.token?.decimals + ) + const frozenToken = formatUnits( + daoDetails?.data?.extra?.frozen_extra_value as any, + daoDetails?.data?.token?.decimals as number + ) + + const contractMethod = contract.methods.propose( + await tezos.wallet.pkh(), + frozenToken, + finalPackedDataBytes + ) + + const result = await contractMethod.send() + + await result.confirmation(1) + window.location.reload() + console.log("RESULT", result) + } catch (error) { + console.log("ERROR", error) + const errorMessage = error instanceof Error ? error.message : "An unknown error occurred" + nofity({ + message: `Error: ${errorMessage}`, + autoHideDuration: 3000, + variant: "error" + }) + } finally { + setIsLoading(false) + } + }} + variant="contained" + disabled={!isValid || isLoading} + > + Submit Form + + + + ) : null} + + ) +} + +export const ArbitraryContractInteractionForm: React.FC<{ + daoLambdas: Array | undefined + showHeader: (state: boolean) => void +}> = ({ daoLambdas, showHeader }) => { + const daoId = useDAOID() + const [aciProposalOpen, setAciProposalOpen] = useState(false) + const isInvalidKtOrTzAddress = (address: string) => validateContractAddress(address) !== 3 + + const initialValue: ACIValues = { + destination_contract: {} as ArbitraryContract, + destination_contract_address: "", + amount: 0, + target_endpoint: "", + parameters: [] + } + + const validateForm = (values: ACIValues) => { + console.log("validateFormValues", values) + return {} + const errors: FormikErrors = {} + if (!values.destination_contract_address) { + errors.destination_contract_address = "Required" + } + if (values.destination_contract_address && isInvalidKtOrTzAddress(values.destination_contract_address)) { + errors.destination_contract_address = "Invalid contract address" + } + if (!values.target_endpoint) { + errors.target_endpoint = "Required" + } + if (values.parameters && values.parameters.length > 0) { + values.parameters.map((param: Parameter, index: number) => { + if (!param.value || param.value === "") { + errors.parameters = [] + errors.parameters[index] = "Required" + errors.parameters.filter(Boolean) + } + }) + } + return errors + } + + const interact = () => { + console.log("saveInfo") + } + + useEffect(() => { + console.log({ daoLambdas }) + }, [daoLambdas]) + + const isAciDeployerDeployed = daoLambdas?.find((lambda: any) => lambda.key === AppConfig.ACI.EXECUTOR_FUNCTION_NAME) + + return ( + + {({ + submitForm, + isSubmitting, + setFieldValue, + values, + errors, + touched, + setFieldTouched, + setFieldError, + isValid + }) => { + return ( +
+ + {daoLambdas !== undefined && ( + setAciProposalOpen(false)} + title={"Proposal to Enable Arbitrary Contract Interaction"} + template="sm" + > + In order to use open-ended Contract Calls, the DAO contract must be amended. + + If you have the minimum amount of tokens for the proposal fee, click Submit to create a proposal for + adding the ACI capability. + + + + + + )} + + ) + }} +
+ ) +} diff --git a/src/modules/explorer/components/CodeCollapse.tsx b/src/modules/explorer/components/CodeCollapse.tsx index 8f9338fc..86fc3ba1 100644 --- a/src/modules/explorer/components/CodeCollapse.tsx +++ b/src/modules/explorer/components/CodeCollapse.tsx @@ -3,20 +3,11 @@ import { ProposalItem } from "modules/explorer/pages/User" import React, { useState } from "react" import { Link } from "react-router-dom" import { Proposal } from "services/services/dao/mappers/proposal/types" -import { ContentContainer } from "./ContentContainer" import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown" import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp" import { ProposalCodeEditorInput } from "./ProposalFormInput" import Prism, { highlight } from "prismjs" - -const TableContainer = styled(ContentContainer)({ - width: "100%" -}) - -const TableHeader = styled(Grid)({ - padding: "16px 46px", - minHeight: 34 -}) +import { TableContainer, TableHeader } from "components/ui/Table" const ProposalsFooter = styled(Grid)({ padding: "16px 46px", diff --git a/src/modules/explorer/components/ConfigProposalForm.tsx b/src/modules/explorer/components/ConfigProposalForm.tsx index 4563f58e..14db4300 100644 --- a/src/modules/explorer/components/ConfigProposalForm.tsx +++ b/src/modules/explorer/components/ConfigProposalForm.tsx @@ -26,13 +26,6 @@ type Values = { export type ProposalFormDefaultValues = RecursivePartial -interface Props { - open: boolean - handleClose: () => void - defaultValues?: ProposalFormDefaultValues - defaultTab?: number -} - const validationSchema = yup.object({ frozen_extra_value: yup.number().typeError("Amount must be a number"), returnedPercentage: yup @@ -42,7 +35,12 @@ const validationSchema = yup.object({ .typeError("Amount must be a number") }) -export const ConfigProposalForm: React.FC = ({ open, handleClose }) => { +export const ConfigProposalForm: React.FC<{ + open: boolean + handleClose: () => void + defaultValues?: ProposalFormDefaultValues + defaultTab?: number +}> = ({ open, handleClose }) => { const daoId = useDAOID() const { data: dao } = useDAO(daoId) diff --git a/src/modules/explorer/components/ConfigProposalFormLambda.tsx b/src/modules/explorer/components/ConfigProposalFormLambda.tsx index cda20d42..0ffd1d33 100644 --- a/src/modules/explorer/components/ConfigProposalFormLambda.tsx +++ b/src/modules/explorer/components/ConfigProposalFormLambda.tsx @@ -1,15 +1,14 @@ -import { Grid, Typography, TextField, styled, CircularProgress } from "@material-ui/core" -import React, { useCallback, useEffect } from "react" +import { Grid, CircularProgress, Typography } from "@material-ui/core" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { useDAO } from "services/services/dao/hooks/useDAO" -import { Controller, FormProvider, useForm } from "react-hook-form" +import { FormProvider, useForm } from "react-hook-form" import { useDAOID } from "../pages/DAO/router" import { ProposalCodeEditorInput, ProposalFormInput } from "./ProposalFormInput" import { ResponsiveDialog } from "./ResponsiveDialog" import Prism, { highlight } from "prismjs" import "prism-themes/themes/prism-night-owl.css" -import { MainButton } from "modules/common/MainButton" +import { StyledSendButton } from "modules/common/StyledSendButton" import { SearchLambda } from "./styled/SearchLambda" -import { CheckOutlined } from "@material-ui/icons" import { useLambdaAddPropose } from "services/contracts/baseDAO/hooks/useLambdaAddPropose" import { useLambdaRemovePropose } from "services/contracts/baseDAO/hooks/useLambdaRemovePropose" import { LambdaDAO } from "services/contracts/baseDAO/lambdaDAO" @@ -17,27 +16,12 @@ import { useDAOLambdas } from "services/contracts/baseDAO/hooks/useDAOLambdas" import { Lambda } from "services/bakingBad/lambdas/types" import { useLambdaExecutePropose } from "services/contracts/baseDAO/hooks/useLambdaExecutePropose" import { parseLambdaCode } from "utils" - -const StyledSendButton = styled(MainButton)(({ theme }) => ({ - width: 101, - color: "#1C1F23" -})) - -const StyledRow = styled(Grid)({ - marginTop: 30 -}) - -const LoadingContainer = styled(Grid)({ - minHeight: 651 -}) - -const LoadingStateLabel = styled(Typography)({ - marginTop: 40 -}) - -const CheckIcon = styled(CheckOutlined)({ - fontSize: 169 -}) +import { ArbitraryContractInteractionForm } from "./ArbitraryContractInteractionForm" +import AppConfig from "config" +import { StyledRow, LoadingContainer, LoadingStateLabel, CheckIcon } from "components/ui/ConfigProposalForm" +import { Link } from "react-router-dom" +import { ViewButton } from "./ViewButton" +import { Button } from "components/ui/Button" const codeEditorcontainerstyles = { marginTop: "8px" @@ -65,19 +49,26 @@ type Values = { lambda_parameters?: Array } +type AciToken = { + counter: number + name?: string + type: string + children: AciToken[] + placeholder?: string + validate?: (value: string) => string | undefined + initValue: tokenValueType +} +type tokenMap = Record<"key" | "value", AciToken> +type tokenValueType = string | boolean | number | AciToken | AciToken[] | tokenMap[] + export enum ProposalAction { new, remove, execute, + aci, none } -interface Props { - open: boolean - action: ProposalAction - handleClose: () => void -} - enum LambdaProposalState { write_action, wallet_action, @@ -138,7 +129,28 @@ Eg:- ` } -export const ProposalFormLambda: React.FC = ({ open, handleClose, action }) => { +const ARBITRARY_CONTRACT_INTERACTION = AppConfig.CONST.ARBITRARY_CONTRACT_INTERACTION + +const ACI: Lambda = { + key: ARBITRARY_CONTRACT_INTERACTION, + id: 4998462, + active: true, + hash: "string", + value: { + code: "[]", + handler_check: "[]", + is_active: false + }, + firstLevel: 4815399, + lastLevel: 4815399, + updates: 1 +} + +export const ProposalFormLambda: React.FC<{ + open: boolean + action: ProposalAction + handleClose: () => void +}> = ({ open, handleClose, action }) => { const grammar = Prism.languages.javascript const daoId = useDAOID() @@ -149,7 +161,11 @@ export const ProposalFormLambda: React.FC = ({ open, handleClose, action const { mutate: lambdaRemove } = useLambdaRemovePropose() const { mutate: lambdaExecute } = useLambdaExecutePropose() + const [aciProposalOpen, setAciProposalOpen] = useState(false) + const [showHeader, setShowHeader] = useState(true) + const lambdaForm = useForm() + const proposalTypeQuery = new URLSearchParams(window.location.search).get("type") const [lambda, setLambda] = React.useState(null) const [state, setState] = React.useState(LambdaProposalState.write_action) @@ -166,10 +182,31 @@ export const ProposalFormLambda: React.FC = ({ open, handleClose, action } }, [open, lambdaForm]) + useEffect(() => { + if (daoLambdas) { + daoLambdas.push(ACI) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [daoLambdas]) + + useEffect(() => { + if (proposalTypeQuery === "add-function") { + setCode(AppConfig.ACI.EXECUTOR_LAMBDA.code) + setAciProposalOpen(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [proposalTypeQuery]) + + useEffect(() => { + if (action === ProposalAction.aci) { + lambdaForm.setValue("lambda_name", ACI.key) + } + }, [action, lambdaForm]) + const onSubmit = useCallback( (_: Values) => { const agoraPostId = Number(0) - + // debugger switch (action) { case ProposalAction.new: { lambdaAdd({ @@ -241,6 +278,12 @@ export const ProposalFormLambda: React.FC = ({ open, handleClose, action [dao, lambdaAdd, code, action, lambda, lambdaRemove, lambdaArguments, lambdaExecute, lambdaParams, handleClose] ) + const isDisabled = useMemo(() => { + if (lambda?.key === ARBITRARY_CONTRACT_INTERACTION) return false + if (!code) return true + if (action === ProposalAction.execute && (!lambda || lambdaArguments === "" || lambdaParams === "")) return true + }, [lambda, code, action, lambdaArguments, lambdaParams]) + const handleSearchChange = (data: Lambda) => { if (!data?.value) { lambdaForm.reset() @@ -300,73 +343,99 @@ export const ProposalFormLambda: React.FC = ({ open, handleClose, action const renderExecuteProposal = () => { return ( <> - - - - setCode(code)} - highlight={code => highlight(code, grammar, "javascript")} - placeholder={codeEditorPlaceholder.existingLambda} - /> - setLambdaArguments(lambdaArguments)} - highlight={lambdaArguments => highlight(lambdaArguments, grammar, "javascript")} - style={codeEditorStyles} - placeholder={codeEditorPlaceholder.lambdaExecuteArgumentsCode} - /> - setLambdaParams(lambdaParams)} - highlight={lambdaParams => highlight(lambdaParams, grammar, "javascript")} - placeholder={codeEditorPlaceholder.lambdaExecuteParams} - /> + {showHeader ? ( + + + + ) : null} + + {lambda?.key !== ARBITRARY_CONTRACT_INTERACTION ? ( + <> + setCode(code)} + highlight={code => highlight(code, grammar, "javascript")} + placeholder={codeEditorPlaceholder.existingLambda} + /> + setLambdaArguments(lambdaArguments)} + highlight={lambdaArguments => highlight(lambdaArguments, grammar, "javascript")} + style={codeEditorStyles} + placeholder={codeEditorPlaceholder.lambdaExecuteArgumentsCode} + /> + setLambdaParams(lambdaParams)} + highlight={lambdaParams => highlight(lambdaParams, grammar, "javascript")} + placeholder={codeEditorPlaceholder.lambdaExecuteParams} + /> + + ) : ( + + )} ) } + const renderAciProposal = () => { + return ( + <> + + + ) + } + + const closeModal = () => { + setShowHeader(true) + handleClose() + } + + const getTitle = (action: ProposalAction) => { + if (action === ProposalAction.aci) { + return "Contract Call Proposal" + } + return ProposalAction[action] + " Function Proposal" + } return ( - + {state === LambdaProposalState.write_action ? ( <> {action === ProposalAction.new ? renderNewProposal() : null} {action === ProposalAction.remove ? renderRemoveProposal() : null} {action === ProposalAction.execute ? renderExecuteProposal() : null} + {action === ProposalAction.aci ? renderAciProposal() : null} - - - Submit - - + {action !== ProposalAction.aci ? ( + + + Submit + + + ) : null} ) : null} diff --git a/src/modules/explorer/components/ContentContainer.tsx b/src/modules/explorer/components/ContentContainer.tsx index 76fdc40b..c0424b09 100644 --- a/src/modules/explorer/components/ContentContainer.tsx +++ b/src/modules/explorer/components/ContentContainer.tsx @@ -1,3 +1,4 @@ +// TODO: Replace imports with components/ui/Table import { styled, Grid } from "@material-ui/core" export const ContentContainer = styled(Grid)(({ theme }) => ({ diff --git a/src/modules/explorer/components/DAOStatsRow.tsx b/src/modules/explorer/components/DAOStatsRow.tsx index 971d72b5..788a2a12 100644 --- a/src/modules/explorer/components/DAOStatsRow.tsx +++ b/src/modules/explorer/components/DAOStatsRow.tsx @@ -121,6 +121,17 @@ export const DAOStatsRow: React.FC = () => { + + + + {symbol} Locked + + + {numbro(amountLocked).format(formatConfig)} + {numbro(amountLockedPercentage).format(formatConfig)}% + + + diff --git a/src/modules/explorer/components/FilterNFTDialog.tsx b/src/modules/explorer/components/FilterNFTDialog.tsx new file mode 100644 index 00000000..c2cdbc18 --- /dev/null +++ b/src/modules/explorer/components/FilterNFTDialog.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TokensFilters } from "../pages/NFTs" + +interface Props { + currentFilters: TokensFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TokensFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& p": { + position: "absolute", + right: 16, + fontWeight: 300 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +export const FilterNFTDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [owner, setOwner] = useState("") + const [valueMin, setValueMin] = useState() + const [valueMax, setValueMax] = useState() + + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + if (currentFilters) { + setOwner(currentFilters?.owner) + setValueMin(currentFilters.valueMin) + setValueMax(currentFilters.valueMax) + } + }, [currentFilters]) + + const showFilters = () => { + const filterObject: TokensFilters = { + owner: owner, + valueMin: valueMin, + valueMax: valueMax + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Value + + + + setValueMin(event.target.value)} + name="test" + value={valueMin} + placeholder="Min" + inputProps={ariaLabel} + type="number" + /> + + + setValueMax(event.target.value)} + name="test" + value={valueMax} + placeholder="Max" + type="number" + inputProps={ariaLabel} + /> + + + + + Creator Address + + + setOwner(event.target.value)} + style={{ width: "100%" }} + name="test" + value={owner} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/FiltersDialog.tsx b/src/modules/explorer/components/FiltersDialog.tsx index 270f5042..f626cb5d 100644 --- a/src/modules/explorer/components/FiltersDialog.tsx +++ b/src/modules/explorer/components/FiltersDialog.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react" import { ResponsiveDialog } from "./ResponsiveDialog" import { Grid, styled } from "@material-ui/core" import { Typography } from "@mui/material" -import { Dropdown } from "./Dropdown" import { ProposalStatus } from "services/services/dao/mappers/proposal/types" import { SmallButton } from "modules/common/SmallButton" import { Order, ProposalType } from "./FiltersUserDialog" diff --git a/src/modules/explorer/components/FiltersTokensDialog.tsx b/src/modules/explorer/components/FiltersTokensDialog.tsx new file mode 100644 index 00000000..18d04c8e --- /dev/null +++ b/src/modules/explorer/components/FiltersTokensDialog.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TokensFilters } from "../pages/Treasury" + +interface Props { + currentFilters: TokensFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TokensFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +export const FilterTokenDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [token, setToken] = useState("") + const [balanceMin, setBalanceMin] = useState() + const [balanceMax, setBalanceMax] = useState() + + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + if (currentFilters) { + setToken(currentFilters?.token) + setBalanceMin(currentFilters.balanceMin) + setBalanceMax(currentFilters.balanceMax) + } + }, [currentFilters]) + + const showFilters = () => { + const filterObject: TokensFilters = { + token: token, + balanceMin: balanceMin, + balanceMax: balanceMax + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Token + + + setToken(event.target.value)} + style={{ width: "40%" }} + name="test" + value={token} + placeholder="Token" + inputProps={ariaLabel} + /> + + + + Balance + + + + setBalanceMin(event.target.value)} + name="test" + value={balanceMin} + placeholder="Min" + inputProps={ariaLabel} + type="number" + /> + + + setBalanceMax(event.target.value)} + name="test" + value={balanceMax} + placeholder="Max" + type="number" + inputProps={ariaLabel} + /> + + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/FiltersTransactionsDialog.tsx b/src/modules/explorer/components/FiltersTransactionsDialog.tsx new file mode 100644 index 00000000..a4c28876 --- /dev/null +++ b/src/modules/explorer/components/FiltersTransactionsDialog.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TransactionsFilters } from "../pages/Treasury" + +export enum TransactionStatus { + COMPLETED = "applied", + PENDING = "pending", + FAILED = "failed" +} + +interface Props { + currentFilters: TransactionsFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TransactionsFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +const StatusButton = styled(Grid)(({ theme }) => ({ + "background": theme.palette.primary.main, + "padding": "8px 16px", + "borderRadius": 50, + "marginRight": 16, + "marginBottom": 16, + "cursor": "pointer", + "textTransform": "capitalize", + "&:hover": { + background: "rgba(129, 254, 183, .4)" + } +})) + +interface StatusOption { + label: string +} + +export const FilterTransactionsDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [status, setStatus] = useState([]) + const [token, setToken] = useState("") + const [sender, setSender] = useState("") + const [receiver, setReceiver] = useState("") + + const [filters, setFilters] = useState() + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + setStatus([]) + setStatusOptions() + if (currentFilters) { + setToken(currentFilters?.token) + setSender(currentFilters.sender) + setReceiver(currentFilters.receiver) + setFilters(currentFilters.status) + } + }, [currentFilters]) + + const setStatusOptions = () => { + const values = Object.values(TransactionStatus) + for (const item in values) { + const obj = { + label: values[item] + } + setStatus(oldArray => [...oldArray, obj]) + } + } + + const isSelected = (item: StatusOption) => { + return filters && filters.label === item.label ? true : false + } + + const saveStatus = (status: StatusOption) => { + if (status.label === filters?.label) { + setFilters(undefined) + } else { + setFilters(status) + } + } + + const showFilters = () => { + const filterObject: TransactionsFilters = { + token: token, + receiver: receiver, + sender: sender, + status: filters + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Sort by + + + + {status.length > 0 && + status.map((item, index) => { + return ( + + saveStatus(item)}>{item.label} + + ) + })} + + + + Token + + + setToken(event.target.value)} + style={{ width: "40%" }} + name="test" + value={token} + placeholder="Token" + inputProps={ariaLabel} + /> + + + + Receiving Address + + + setReceiver(event.target.value)} + name="test" + value={receiver} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + Sending Address + + + setSender(event.target.value)} + name="test" + value={sender} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/Hero.tsx b/src/modules/explorer/components/Hero.tsx index 38ea79f7..a3855610 100644 --- a/src/modules/explorer/components/Hero.tsx +++ b/src/modules/explorer/components/Hero.tsx @@ -1,6 +1,6 @@ -import { Grid, GridProps, styled } from "@material-ui/core" import React from "react" -import { ContentContainer } from "./ContentContainer" +import { Grid, GridProps, styled } from "@material-ui/core" +import { ContentContainer } from "components/ui/Table" const Container = styled(ContentContainer)({ "padding": "0px", diff --git a/src/modules/explorer/components/ProposalActionsDialog.tsx b/src/modules/explorer/components/ProposalActionsDialog.tsx index d3757979..16fce696 100644 --- a/src/modules/explorer/components/ProposalActionsDialog.tsx +++ b/src/modules/explorer/components/ProposalActionsDialog.tsx @@ -1,7 +1,7 @@ import { Grid, styled, Typography, useMediaQuery, useTheme } from "@material-ui/core" import { RegistryProposalFormValues } from "modules/explorer/components/UpdateRegistryDialog" import { TreasuryProposalFormValues } from "modules/explorer/components/NewTreasuryProposalDialog" -import React, { useState } from "react" +import React, { useCallback, useEffect, useState } from "react" import { NFTTransferFormValues } from "./NFTTransfer" import { useDAOID } from "../pages/DAO/router" import { ConfigProposalForm } from "./ConfigProposalForm" @@ -14,6 +14,7 @@ import { useDAO } from "services/services/dao/hooks/useDAO" import { ProposalCreatorModal } from "modules/lite/explorer/pages/CreateProposal/ProposalCreatorModal" import { useIsProposalButtonDisabled } from "services/contracts/baseDAO/hooks/useCycleInfo" import { ProposalFormContainer } from "./ProposalForm" +import { useQueryParams } from "../hooks/useQueryParams" type RecursivePartial = { [P in keyof T]?: RecursivePartial @@ -34,7 +35,7 @@ const OptionContainer = styled(Grid)(({ theme }) => ({ "padding": "35px 42px", "marginBottom": 16, "cursor": "pointer", - "height": 110, + "height": 80, "&:hover:enabled": { background: theme.palette.secondary.dark, scale: 1.01, @@ -107,6 +108,12 @@ const getActions = (): Action[] => [ id: ProposalAction.execute, isLambda: true }, + { + name: "Arbitrary Contract Interaction", + description: "Interact with any contract on Tezos.", + id: ProposalAction.aci, + isLambda: true + }, { name: "Off Chain Poll", description: "Create an off-chain poll for your community.", @@ -136,15 +143,17 @@ const getTreasuryActions = (): GenericAction[] => [ interface Props { open: boolean handleClose: () => void + queryType: string | null } const defaultOpenSupportedExecuteProposalModal = "none" -export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => { +export const ProposalActionsDialog: React.FC = ({ open, handleClose, queryType }) => { const daoId = useDAOID() const { data } = useDAO(daoId) const theme = useTheme() const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) + const { clearParams } = useQueryParams() const [proposalAction, setProposalAction] = useState(ProposalAction.none) const [openProposalFormLambda, setOpenProposalFormLambda] = useState(false) @@ -153,14 +162,19 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => const liteDAOId = data?.liteDAOData?._id const shouldDisable = useIsProposalButtonDisabled(daoId) + const proposalActions = getActions() - const handleOpenCustomProposalModal = (key: ProposalAction) => { - setProposalAction(key) - setOpenProposalFormLambda(true) - handleClose() - } + const handleOpenCustomProposalModal = useCallback( + (key: ProposalAction) => { + setProposalAction(key) + setOpenProposalFormLambda(true) + handleClose() + }, + [handleClose] + ) const handleCloseCustomProposalModal = () => { + clearParams() setProposalAction(ProposalAction.none) setOpenProposalFormLambda(false) handleClose() @@ -192,10 +206,21 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => handleClose() } + const handleAciProposal = () => { + handleOpenCustomProposalModal(ProposalAction.aci) + handleClose() + } + const [openSupportedExecuteProposalModalKey, setOpenSupportedExecuteProposalModal] = useState( defaultOpenSupportedExecuteProposalModal ) + useEffect(() => { + if (queryType === "add-function") { + handleOpenCustomProposalModal(ProposalAction.new) + } + }, [handleOpenCustomProposalModal, queryType]) + return ( <> @@ -204,34 +229,32 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => Configuration Proposal - {getActions() - .slice(0, 3) - .map((elem, index) => - !liteDAOId && elem.id === "off-chain" ? null : ( - - - elem.id === "off-chain" - ? handleLiteProposal() - : !shouldDisable - ? elem.isLambda - ? handleOpenCustomProposalModal(elem.id) - : handleOpenSupportedExecuteProposalModal(elem.id) - : null - } + {proposalActions.slice(0, 3).map((elem, index) => + !liteDAOId && elem.id === "off-chain" ? null : ( + + + elem.id === "off-chain" + ? handleLiteProposal() + : !shouldDisable + ? elem.isLambda + ? handleOpenCustomProposalModal(elem.id) + : handleOpenSupportedExecuteProposalModal(elem.id) + : null + } + > + + {elem.name} + + - - {elem.name} - - - {elem.description}{" "} - - - - ) - )} + {elem.description}{" "} + + + + ) + )} @@ -258,7 +281,7 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => Off-Chain Proposal - {getActions() + {proposalActions .filter(item => item.id === "off-chain") .map((elem, index) => !liteDAOId && elem.id !== "off-chain" ? null : ( @@ -272,39 +295,52 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => )} + + + Arbitrary Contract Interaction + + + + !shouldDisable && handleAciProposal()}> + Contract Call + + Invoke an endpoint on a deployed contract + + + + + Function Proposal - {getActions() - .slice(3, 6) - .map((elem, index) => - !liteDAOId && elem.id === "off-chain" ? null : ( - - - elem.id === "off-chain" - ? handleLiteProposal() - : !shouldDisable - ? elem.isLambda - ? handleOpenCustomProposalModal(elem.id) - : handleOpenSupportedExecuteProposalModal(elem.id) - : null - } + {proposalActions.slice(3, 6).map((elem, index) => + !liteDAOId && elem.id === "off-chain" ? null : ( + + + elem.id === "off-chain" + ? handleLiteProposal() + : !shouldDisable + ? elem.isLambda + ? handleOpenCustomProposalModal(elem.id) + : handleOpenSupportedExecuteProposalModal(elem.id) + : null + } + > + + {elem.name} + + - - {elem.name} - - - {elem.description}{" "} - - - - ) - )} + {elem.description}{" "} + + + + ) + )} diff --git a/src/modules/explorer/components/ProposalExecuteForm.tsx b/src/modules/explorer/components/ProposalExecuteForm.tsx new file mode 100644 index 00000000..44d201c2 --- /dev/null +++ b/src/modules/explorer/components/ProposalExecuteForm.tsx @@ -0,0 +1,109 @@ +import { Form, Formik } from "formik" +import React, { useEffect } from "react" +import { genLambda, parseContract } from "../../../services/aci" +import { RenderItem } from "./aci/Fields" +import type { TezosToolkit } from "@taquito/taquito" +import type { BeaconWallet } from "@taquito/beacon-wallet" +import type BigNumber from "bignumber.js" +import { useTezos } from "services/beacon/hooks/useTezos" +import { SmallButtonDialog } from "modules/common/SmallButton" +import { useDAOID } from "../pages/DAO/router" +import { useDAO } from "services/services/dao/hooks/useDAO" + +type contractStorage = { version: string } & { + [key: string]: any + proposal_counter: BigNumber + balance: string + threshold: BigNumber + owners: Array +} + +type tezosState = { + beaconWallet: BeaconWallet + contracts: any // DAO Contracts + address: string | null // Logged in User Address + balance: string | null // Logged in user balance + currentContract: string | null // Contract Address + currentStorage: contractStorage | null +} + +function ProposalExecuteForm( + props: React.PropsWithoutRef<{ + address: string // Input contract Address + amount: number + shape: any + reset: () => void + setField: (lambda: string, metadata: string) => void + setLoading: (x: boolean) => void + setState: (shape: any) => void + onReset?: () => void + loading: boolean + onShapeChange: (v: object) => void + }> +) { + const daoId = useDAOID() + const { data: dao } = useDAO(daoId) + + const address = props.address + const loading = props.loading + const { tezos } = useTezos() + + useEffect(() => { + if (!Object.keys(props.shape).length && !loading) { + ;(async () => { + try { + const c = await tezos.contract.at(address) + const initTokenTable: Record = {} + const token = await parseContract(c, initTokenTable) + props.setState({ init: initTokenTable, token, contract: c }) + } catch (e) { + console.error("Error fetching contract:", e) + } + })() + } + }, [address, loading, props, props.shape, tezos.contract]) + + return ( +
+ {}} + validateOnMount={true} + validate={values => { + props.onShapeChange(values) + try { + // ACI: This sets the lambda and metadata fields + if (dao?.data?.address) genLambda("1.0.0", props, values) + } catch (e) { + // setSubmitError((e as Error).message); + } + }} + > + {_ => { + return ( +
+
+ {!!props.shape.token && } +
+ {/*
+ { + e.preventDefault() + props.reset() + props.onReset?.() + }} + > + Reset + +
*/} +
+ ) + }} +
+
+ ) +} +export default ProposalExecuteForm diff --git a/src/modules/explorer/components/ResponsiveDialog.tsx b/src/modules/explorer/components/ResponsiveDialog.tsx index d0522987..3e7cca8e 100644 --- a/src/modules/explorer/components/ResponsiveDialog.tsx +++ b/src/modules/explorer/components/ResponsiveDialog.tsx @@ -11,7 +11,7 @@ const Content = styled(Grid)({ const TitleText = styled(Typography)(({ theme }) => ({ color: "#ffff", fontWeight: 600, - lineHeight: ".80", + lineHeight: "1.2", textTransform: "capitalize", fontSize: 24, [theme.breakpoints.down("sm")]: { @@ -51,7 +51,7 @@ export const ResponsiveDialog: React.FC<{ return isSmall ? ( - + {onGoBack !== undefined ? ( @@ -72,7 +72,15 @@ export const ResponsiveDialog: React.FC<{ ) : ( - + { + // TODO: Comment this while creating PR + if (reason && reason === "backdropClick") return + onClose() + }} + maxWidth={template} + > {onGoBack !== undefined ? ( diff --git a/src/modules/explorer/components/SearchEndpoints.tsx b/src/modules/explorer/components/SearchEndpoints.tsx new file mode 100644 index 00000000..beb8ad2d --- /dev/null +++ b/src/modules/explorer/components/SearchEndpoints.tsx @@ -0,0 +1,180 @@ +import React, { useEffect, useState } from "react" +import { Grid, InputAdornment, makeStyles, styled, TextField, Theme, withStyles } from "@material-ui/core" +import { SearchOutlined } from "@material-ui/icons" +import { Autocomplete } from "@material-ui/lab" +import { ArbitraryContract } from "models/Contract" + +export interface ContractEndpoint { + name: string + type: string + params: ContractParam[] +} +export interface ContractParam { + placeholder: string + type: string +} + +const StyledType = styled(Grid)({ + opacity: 0.65 +}) + +const StyledInput = withStyles((theme: Theme) => ({ + popperDisablePortal: { + width: "418px !important", + left: 46, + marginTop: -2 + }, + popper: { + "& div.MuiPaper-root": { + "borderTopRightRadius": 0, + "borderTopLeftRadius": 0, + "marginTop": -1, + "background": "#24282b", + "& div.MuiAutocomplete-paper": { + "& ul": { + "background": "inherit", + + "& li": { + borderBottom: "1px solid gray", + paddingBbottom: 12 + } + } + } + } + }, + root: { + "& div.MuiFormControl-root": { + "& div.MuiInputBase-root": { + "padding": 0, + "marginTop": 0, + "& div.MuiAutocomplete-endAdornment": { + "& button.MuiButtonBase-root": { + color: theme.palette.text.primary + } + } + }, + "& label.MuiFormLabel-root": { + marginLeft: 36, + marginTop: -3, + color: theme.palette.text.primary, + opacity: 0.65 + } + } + } +}))(Autocomplete) + +const SearchIcon = styled(SearchOutlined)({ + marginRight: 5 +}) + +const useStyles = makeStyles({ + "@global": { + ".MuiAutocomplete-option:not(:last-child)": { + borderBottom: "0.3px solid #7d8c8b", + paddingTop: 12 + }, + ".MuiAutocomplete-option": { + paddingBottom: 12, + paddingTop: 12 + }, + ".MuiAutocomplete-listbox": { + padding: 0, + maxHeight: 442 + } + } +}) + +export const SearchEndpoints: React.FC<{ + endpoints: Array | undefined + handleChange?: any +}> = ({ endpoints, handleChange }) => { + useStyles() + + const [formattedEndpoints, setFormattedEndpoints] = useState() + + useEffect(() => { + const handleEndpointStructure = () => { + const formattedData = endpoints?.map(item => { + const endpoint: ContractEndpoint = { + name: item.name, + type: item.type, + params: [] + } + switch (item.type) { + case "unit": + break + case "address": + const param = { + type: "address", + placeholder: "address" + } + endpoint.params.push(param) + break + case "pair": + item.children.map((child: any) => { + const pairParam = { + type: child.type, + placeholder: child.name + } + endpoint.params.push(pairParam) + }) + break + case "bool": + const paramBool = { + type: "bool", + placeholder: "bool" + } + endpoint.params.push(paramBool) + break + default: + const paramDefault = { + type: item.type, + placeholder: item.type + } + endpoint.params.push(paramDefault) + break + } + return endpoint + }) + return formattedData + } + + const data = handleEndpointStructure() + setFormattedEndpoints(data) + }, [endpoints]) + + return ( + <> + {formattedEndpoints ? ( + option.name} + renderOption={(option: any, state: any) => ( + + {option.name} + {option.type} + + )} + renderInput={params => ( + + + + ), + disableUnderline: true + }} + /> + )} + onChange={(e: any, data: any) => handleChange(data)} + /> + ) : null} + + ) +} diff --git a/src/modules/explorer/components/UserBalances.tsx b/src/modules/explorer/components/UserBalances.tsx index e640b96c..0a626a65 100644 --- a/src/modules/explorer/components/UserBalances.tsx +++ b/src/modules/explorer/components/UserBalances.tsx @@ -4,20 +4,6 @@ import { useTezos } from "services/beacon/hooks/useTezos" import { useDAO } from "services/services/dao/hooks/useDAO" import CancelIcon from "@mui/icons-material/Cancel" -const BalancesBox = styled(Grid)(({ theme }) => ({ - minHeight: "137px", - maxHeight: "344px", - padding: "38px 38px", - background: theme.palette.primary.main, - boxSizing: "border-box", - borderRadius: 8, - boxShadow: "none", - - ["@media (max-width:409.99px)"]: { - maxHeight: "325px" - } -})) - interface Balances { available: { displayName: string diff --git a/src/modules/explorer/components/aci/Fields.tsx b/src/modules/explorer/components/aci/Fields.tsx new file mode 100644 index 00000000..335994b2 --- /dev/null +++ b/src/modules/explorer/components/aci/Fields.tsx @@ -0,0 +1,511 @@ +import React from "react" +import assertNever from "assert-never" +import { Field, FieldArray, FieldProps, Form, Formik, useFormikContext } from "formik" +import type { token, tokenMap, tokenValueType } from "../../../../services/aci" +import { showName, getFieldName, allocateNewTokenCounter } from "../../../../services/aci" +import { styled, Typography } from "@material-ui/core" +import { ProposalFormInput } from "../ProposalFormInput" +import { Button } from "components/ui/Button" + +const Title = styled(Typography)({ + fontSize: 18, + fontWeight: 450 +}) + +function capitalizeFirstLetter(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1) +} +function RenderItem({ + token: token, + showTitle: showTitle +}: React.PropsWithoutRef<{ + token: token + showTitle: boolean +}>) { + // debugger + const { setFieldValue, getFieldProps } = useFormikContext>() + const counter: number = getFieldProps("counter").value + const fieldName = getFieldName(token.counter) + const fieldValue: tokenValueType = getFieldProps(fieldName).value + + try { + switch (token.type) { + case "bls12_381_fr": + case "bls12_381_g1": + case "bls12_381_g2": + case "chain_id": + case "key_hash": + case "key": + case "bytes": + case "address": + case "signature": + case "string": + case "contract": + case "int": + case "nat": + case "mutez": + case "timestamp": + case "sapling_transaction_deprecated": + case "sapling_transaction": + case "sapling_state": + return RenderInputField(token, fieldName, showTitle) + case "never": + case "unit": + return RenderConstant(token, showTitle) + case "bool": + return RenderCheckbox(token, fieldName, fieldValue, showTitle) + case "or": + return RenderSelection(token, fieldName, fieldValue, showTitle) + case "set": + case "list": + return RenderArray(token, fieldName, fieldValue, showTitle, counter, setFieldValue) + case "pair": + return RenderPair(token, showTitle) + case "map": + return RenderMap(token, fieldName, fieldValue, showTitle, counter, setFieldValue) + case "option": + return RenderOption(token, fieldName, fieldValue, showTitle) + case "lambda": + return RenderLambda(token, fieldName, showTitle) + case "ticket_deprecated": + case "ticket": + case "operation": + case "chest": + case "chest_key": + case "tx_rollup_l2_address": + case "constant": + case "big_map": + return RenderNonsupport(token) + default: + return assertNever(token.type) + } + } catch (e) { + return null + // return renderError((e as Error).message, true) + } +} + +function RenderInputField(token: token, fieldName: string, showTitle: boolean) { + return ( +
+ + + + {/* */} +
+ ) +} + +function RenderConstant(token: token, showTitle: boolean) { + return ( +
+ +
+

{capitalizeFirstLetter(token.type)}

+
+
+ ) +} + +// This can be improved with material checkbox +function RenderCheckbox(token: token, fieldName: string, values: tokenValueType, showTitle: boolean) { + if (typeof values !== "boolean") { + throw new Error("internal error: the value of bool is incorrect") + } else { + return ( +
+ +
+ {" "} + {capitalizeFirstLetter(`${values}`)} +
+
+ ) + } +} + +function RenderArray( + token: token, + fieldName: string, + elements: tokenValueType, + showTitle: boolean, + counter: number, + setFieldValue: (field: string, value: tokenValueType, shouldValidate?: boolean | undefined) => void +) { + if (!Array.isArray(elements)) { + throw new Error("internal error: the value of array is incorrect") + } + return ( +
+ + + {({ push, pop }) => { + return ( +
+ {elements && + elements.map((v, idx) => { + if (!("counter" in v)) { + throw new Error("internal error: the value of array is incorrect") + } + return ( +
+ +
+ ) + })} +
+ {elements && elements.length > 0 && ( + + )} + +
+
+ ) + }} +
+
+ ) +} + +function RenderPair(token: token, showTitle: boolean) { + return ( +
+ + { +
+ {token.children.map((v, idx) => { + return ( +
+ +
+ ) + })} +
+ } +
+ ) +} + +function RenderOption(token: token, fieldName: string, value: tokenValueType, showTitle: boolean) { + if (typeof value !== "string") { + throw new Error("internal error: the value of option is incorrect") + } + return ( +
+ + + + + + {value == "some" ? :
} +
+ ) +} + +function RenderLambda(token: token, fieldName: string, showTitle: boolean) { + return ( +
+ + + {/* */} +
+ ) +} + +function RenderNonsupport(token: token) { + return ( +
+ {`Type, ${token.type}, isn't supported as a user input`} +
+ ) +} + +function RenderMap( + token: token, + fieldName: string, + elements: tokenValueType, + showTitle: boolean, + counter: number, + setFieldValue: (field: string, value: tokenValueType, shouldValidate?: boolean | undefined) => void +) { + if (!Array.isArray(elements)) { + throw new Error("internal: the value of array is incorrect") + } + return ( +
+ + + {({ push, pop }) => { + return ( +
+ {elements && + elements.map((element, idx) => { + if ("counter" in element) { + throw new Error("internal error: the value of array is incorrect") + } + return ( +
+ + +
+ ) + })} +
+ {elements && elements.length > 0 && ( + + )} + +
+
+ ) + }} +
+
+ ) +} + +function RenderSelection(token: token, fieldName: string, selected: tokenValueType, showTitle: boolean) { + const { setFieldValue, setFieldError } = useFormikContext() + + const defaultChildToken = token.children.length > 0 ? token.children[0] : undefined + const childToken = + token.children.find(x => { + return selected && x.name == selected + }) || defaultChildToken + + // console.log("OldSelectToken", token) + + return ( +
+ {showTitle && showName(token.type, token.name)} + + {({ field }: FieldProps) => ( + + + + )} + + {childToken ? :
} +
+ ) + + // return ( + //
+ // + // + // {({ field }: FieldProps) => ( + // + // )} + // + // {childToken ? :
} + //
+ // ) +} + +export { + RenderInputField, + RenderConstant, + RenderCheckbox, + RenderArray, + RenderPair, + RenderMap, + RenderOption, + RenderLambda, + RenderNonsupport, + RenderItem, + RenderSelection +} diff --git a/src/modules/explorer/hooks/useQueryParams.ts b/src/modules/explorer/hooks/useQueryParams.ts new file mode 100644 index 00000000..38557721 --- /dev/null +++ b/src/modules/explorer/hooks/useQueryParams.ts @@ -0,0 +1,48 @@ +// In react-router-dom v6, useHistory is deprecated and useNavigate is recommended. +// react-router-dom v5 is used in this project. +import { useCallback, useMemo } from "react" +import { useLocation, useHistory } from "react-router-dom" + +const useQuery = () => { + const { search } = useLocation() + return useMemo(() => new URLSearchParams(search), [search]) +} + +export const useQueryParams = >() => { + const location = useLocation() + const history = useHistory() + + const searchParams = useQuery() + + const getParam = useCallback( + (key: keyof T) => { + return searchParams.get(key as string) as T[keyof T] | null + }, + [searchParams] + ) + + const setParam = useCallback( + (key: keyof T, value: T[keyof T]) => { + history.replace({ pathname: location.pathname, search: searchParams.toString() }) + }, + [history, location.pathname, searchParams] + ) + + const removeParam = useCallback( + (key: keyof T) => { + history.replace({ pathname: location.pathname, search: searchParams.toString() }) + }, + [history, location.pathname, searchParams] + ) + + const clearParams = useCallback(() => { + history.replace({ pathname: location.pathname, search: "" }) + }, [history, location.pathname]) + + return { + getParam, + setParam, + removeParam, + clearParams + } +} diff --git a/src/modules/explorer/pages/Config/components/DAOInfoTable.tsx b/src/modules/explorer/pages/Config/components/DAOInfoTable.tsx index dd25a41e..3e9ad93f 100644 --- a/src/modules/explorer/pages/Config/components/DAOInfoTable.tsx +++ b/src/modules/explorer/pages/Config/components/DAOInfoTable.tsx @@ -6,7 +6,6 @@ import { TableBody, TableCell, TableContainer, - TableHead, TableRow, Typography, useMediaQuery, @@ -28,14 +27,6 @@ const RowValue = styled(Typography)(({ theme }) => ({ } })) -const TableTitle = styled(Typography)(({ theme }) => ({ - fontWeight: 500, - fontSize: 18, - [theme.breakpoints.down("sm")]: { - fontSize: 16 - } -})) - const CustomTableContainer = styled(TableContainer)(({ theme }) => ({ width: "inherit", [theme.breakpoints.down("sm")]: {} @@ -49,12 +40,6 @@ const CustomTableCell = styled(TableCell)(({ theme }) => ({ } })) -const CustomTableCellTitle = styled(TableCell)(({ theme }) => ({ - [theme.breakpoints.down("sm")]: { - paddingLeft: "16px !important" - } -})) - const CustomTableCellValue = styled(TableCell)(({ theme }) => ({ [theme.breakpoints.down("sm")]: { paddingTop: 0, diff --git a/src/modules/explorer/pages/Config/index.tsx b/src/modules/explorer/pages/Config/index.tsx index 00f46d83..bffa900c 100644 --- a/src/modules/explorer/pages/Config/index.tsx +++ b/src/modules/explorer/pages/Config/index.tsx @@ -8,7 +8,6 @@ import { useDAOID } from "../DAO/router" import { useDropAllExpired } from "../../../../services/contracts/baseDAO/hooks/useDropAllExpired" import { SmallButton } from "../../../common/SmallButton" -import { ContentContainer } from "../../components/ContentContainer" import { InfoIcon } from "../../components/styled/InfoIcon" import { CopyAddress } from "modules/common/CopyAddress" import { HeroTitle } from "modules/explorer/components/HeroTitle" @@ -21,6 +20,7 @@ import { DaoInfoTables } from "./components/DAOInfoTable" import { ProposalStatus } from "services/services/dao/mappers/proposal/types" import { ProposalCreator } from "modules/lite/explorer/pages/CreateProposal" import { ProposalCreatorModal } from "modules/lite/explorer/pages/CreateProposal/ProposalCreatorModal" +import { ContentContainer } from "components/ui/Table" interface Action { id: any diff --git a/src/modules/explorer/pages/DAO/index.tsx b/src/modules/explorer/pages/DAO/index.tsx index a2e975d9..a7d173eb 100644 --- a/src/modules/explorer/pages/DAO/index.tsx +++ b/src/modules/explorer/pages/DAO/index.tsx @@ -1,23 +1,19 @@ import React, { useMemo, useState } from "react" -import { Grid, styled, Typography, Button, useTheme, useMediaQuery, Avatar } from "@material-ui/core" +import BigNumber from "bignumber.js" +import { Grid, styled, Typography, useTheme, useMediaQuery, Avatar } from "@material-ui/core" -import { useFlush } from "services/contracts/baseDAO/hooks/useFlush" import { useDAO } from "services/services/dao/hooks/useDAO" -import { useProposals } from "services/services/dao/hooks/useProposals" import { useDAOID } from "./router" -import { ContentContainer } from "../../components/ContentContainer" -import { ProposalsList } from "../../components/ProposalsList" -import { ProposalStatus } from "services/services/dao/mappers/proposal/types" import { DAOStatsRow } from "../../components/DAOStatsRow" import { UsersTable } from "../../components/UsersTable" -import BigNumber from "bignumber.js" + import { SmallButton } from "../../../common/SmallButton" -import { usePolls } from "modules/lite/explorer/hooks/usePolls" -import dayjs from "dayjs" + import { DaoSettingModal } from "./components/Settings" import SettingsIcon from "@mui/icons-material/Settings" import { SettingsDialog } from "./components/SettingsDialog" +import { ContentContainer } from "components/ui/Table" export const StyledAvatar = styled(Avatar)({ height: 50, diff --git a/src/modules/explorer/pages/DAOList/index.tsx b/src/modules/explorer/pages/DAOList/index.tsx index e75f56a3..0220bc54 100644 --- a/src/modules/explorer/pages/DAOList/index.tsx +++ b/src/modules/explorer/pages/DAOList/index.tsx @@ -114,7 +114,7 @@ const TabsContainer = styled(Grid)(({ theme }) => ({ })) export const DAOList: React.FC = () => { - const { network, account, etherlink } = useTezos() + const { network, etherlink, account } = useTezos() const { data: daos, isLoading } = useAllDAOs(network) const theme = useTheme() diff --git a/src/modules/explorer/pages/NFTs/index.tsx b/src/modules/explorer/pages/NFTs/index.tsx index b27385f8..c4a13dfd 100644 --- a/src/modules/explorer/pages/NFTs/index.tsx +++ b/src/modules/explorer/pages/NFTs/index.tsx @@ -6,7 +6,7 @@ import { NFTDialog } from "modules/explorer/components/NFTDialog" import { ProposalFormContainer, ProposalFormDefaultValues } from "modules/explorer/components/ProposalForm" import { UserBadge } from "modules/explorer/components/UserBadge" -import React, { useState } from "react" +import React, { useMemo, useState } from "react" import { NFTDAOHolding } from "services/bakingBad/tokenBalances" import { useTezos } from "services/beacon/hooks/useTezos" import { useDAONFTHoldings } from "services/contracts/baseDAO/hooks/useDAOHoldings" @@ -17,7 +17,11 @@ import { useIsProposalButtonDisabled } from "../../../../services/contracts/base import { SmallButton } from "../../../common/SmallButton" import { parseUnits } from "services/contracts/utils" import ReactPaginate from "react-paginate" +import FilterAltIcon from "@mui/icons-material/FilterAlt" + import "../DAOList/styles.css" +import { SearchInput } from "../DAOList/components/Searchbar" +import { FilterNFTDialog } from "modules/explorer/components/FilterNFTDialog" const Card = styled(ContentContainer)(({ theme }) => ({ boxSizing: "border-box", @@ -46,11 +50,16 @@ const NFTTitle = styled(Typography)({ fontWeight: 500 }) -const ProposalsFooter = styled(Grid)({ - padding: "16px 46px", - minHeight: 34 +const FiltersContainer = styled(Grid)({ + cursor: "pointer" }) +export interface TokensFilters { + owner: string | null + valueMin: string | undefined + valueMax: string | undefined +} + export const NFTs: React.FC = () => { const theme = useTheme() const daoId = useDAOID() @@ -61,6 +70,9 @@ export const NFTs: React.FC = () => { const [defaultValues, setDefaultValues] = useState() const [selectedNFT, setSelectedNFT] = useState() const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() const onClickNFT = (nft: NFTDAOHolding) => { setSelectedNFT(nft) @@ -94,7 +106,7 @@ export const NFTs: React.FC = () => { const onCloseTransfer = () => { setOpenTransfer(false) } - const value = isMobileSmall ? 6 : 3 + const value = isMobileSmall ? 6 : 4 const shouldDisable = useIsProposalButtonDisabled(daoId) const [currentPage, setCurrentPage] = useState(0) @@ -105,17 +117,80 @@ export const NFTs: React.FC = () => { const newOffset = (event.selected * value) % nftHoldings.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } + const filterByName = (text: string) => { + setSearchText(text.trim()) + } + + const handleFilters = (filters: TokensFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + + const rows = useMemo(() => { + const handleFilterData = (holdings: NFTDAOHolding[]) => { + let data = holdings.slice() + if (filters?.owner && filters.owner !== "") { + data = holdings.filter(trx => + trx.token?.firstCreator ? trx.token?.firstCreator.toLowerCase() === filters.owner?.toLocaleLowerCase() : null + ) + } + if (filters?.valueMin && filters.valueMin !== "") { + data = holdings.filter(trx => trx.balance.isGreaterThanOrEqualTo(filters.valueMin!)) + } + if (filters?.valueMax && filters.valueMax !== "") { + data = holdings.filter(trx => trx.balance.isLessThanOrEqualTo(filters.valueMax!)) + } + return data + } + + if (!nftHoldings) { + return [] + } + let holdings = nftHoldings.slice() + + if (filters) { + holdings = handleFilterData(holdings) + } + + if (searchText) { + holdings = holdings.filter( + holding => holding.token && holding.token.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + return holdings + }, [nftHoldings, searchText, filters]) + const pageCount = Math.ceil(nftHoldings ? nftHoldings.length / value : 0) return ( <> - + + + setOpenFiltersDialog(true)} + xs={isMobileSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + - {!nftHoldings ? ( + {!rows ? ( <> @@ -123,7 +198,7 @@ export const NFTs: React.FC = () => { ) : ( <> - {nftHoldings.slice(offset, offset + value).map((nft, i) => ( + {rows.slice(offset, offset + value).map((nft, i) => ( { ))} - {!(nftHoldings && nftHoldings.length > 0) ? ( - + {!(rows && rows.length > 0) ? ( + - + No items - + ) : null} { defaultValues={defaultValues} defaultTab={1} /> + ) } diff --git a/src/modules/explorer/pages/Proposals/index.tsx b/src/modules/explorer/pages/Proposals/index.tsx index c6a9045e..312cc18f 100644 --- a/src/modules/explorer/pages/Proposals/index.tsx +++ b/src/modules/explorer/pages/Proposals/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react" +import React, { useCallback, useEffect, useState } from "react" import { Button, Grid, styled, Theme, Typography, useMediaQuery, useTheme } from "@material-ui/core" import { useDAO } from "services/services/dao/hooks/useDAO" @@ -111,6 +111,7 @@ export const Proposals: React.FC = () => { const { data: proposals } = useProposals(daoId) const theme = useTheme() const isMobileSmall = useMediaQuery(theme.breakpoints.down("xs")) + const proposalTypeQuery = new URLSearchParams(window.location.search).get("type") const [openDialog, setOpenDialog] = useState(false) const [openFiltersDialog, setOpenFiltersDialog] = useState(false) @@ -160,6 +161,12 @@ export const Proposals: React.FC = () => { setFilters(filters) } + useEffect(() => { + if (proposalTypeQuery === "add-function") { + setOpenDialog(true) + } + }, [proposalTypeQuery]) + return ( <> @@ -261,7 +268,7 @@ export const Proposals: React.FC = () => { direction="row" alignItems="center" > - + Filter & Sort @@ -312,7 +319,9 @@ export const Proposals: React.FC = () => { - + + + {/* Keeping this component here as it is inhe master branch */} ({ color: theme.palette.secondary.main, @@ -20,17 +28,6 @@ const TokenSymbol = styled(Typography)(({ theme }) => ({ fontSize: 24 })) -const MobileTableHeader = styled(Grid)({ - width: "100%", - padding: 20, - borderBottom: "0.3px solid #3D3D3D" -}) - -const MobileTableRow = styled(Grid)({ - padding: "30px", - borderBottom: "0.3px solid #3D3D3D" -}) - interface RowData { symbol: string address: string @@ -78,17 +75,6 @@ const createData = (daoHolding: DAOHolding): RowData => { const titles = ["Token Balances", "Address", "Balance"] as const -const titleDataMatcher = (title: (typeof titles)[number], rowData: RowData) => { - switch (title) { - case "Token Balances": - return rowData.symbol - case "Address": - return rowData.address - case "Balance": - return rowData.amount - } -} - interface TableProps { rows: RowData[] tezosBalance: BigNumber @@ -100,7 +86,6 @@ interface TableProps { const BalancesList: React.FC = ({ rows, - tezosBalance, openTokenTransferModal, openXTZTransferModal, shouldDisable, @@ -108,85 +93,82 @@ const BalancesList: React.FC = ({ }) => { const [currentPage, setCurrentPage] = useState(0) const [offset, setOffset] = useState(0) - const value = isMobileSmall ? 6 : 2 - // Invoke when user click to request another page. + const value = isMobileSmall ? 6 : 5 + const [list, setList] = useState(rows) + + useEffect(() => { + setList(rows) + }, [rows]) + const handlePageClick = (event: { selected: number }) => { if (rows) { const newOffset = (event.selected * value) % rows.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(rows ? rows.length / value : 0) return ( - - - - XTZ - - - Balance - - {tezosBalance.toString()} - - openXTZTransferModal()} - disabled={shouldDisable} - > - Transfer - + {list && list.length > 0 ? ( + <> + {list.slice(offset, offset + value).map((row, i) => ( + + + + {row.symbol} + + {toShortAddress(row.address)} + + + + Balance + + {row.amount} + + + row.symbol === "XTZ" ? openXTZTransferModal() : openTokenTransferModal(row.address) + } + disabled={shouldDisable} + > + Transfer + + + + - - - - - {rows.slice(offset, offset + value).map((row, i) => ( - - - - {row.symbol} - - {toShortAddress(row.address)} - - - - Balance - - {row.amount} - - openTokenTransferModal(row.address)} - disabled={shouldDisable} - > - Transfer - - - - - - ))} - - - + ))} + + + + + ) : ( + No items + )} ) } @@ -200,6 +182,13 @@ export const BalancesTable: React.FC = () => { const { data: tezosBalance } = useTezosBalance(daoId) const [openTransfer, setOpenTransfer] = useState(false) const [defaultValues, setDefaultValues] = useState() + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() + + const filterByName = (text: string) => { + setSearchText(text.trim()) + } const onCloseTransfer = () => { setOpenTransfer(false) @@ -245,17 +234,81 @@ export const BalancesTable: React.FC = () => { setOpenTransfer(true) } + const handleFilters = (filters: TokensFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + const rows = useMemo(() => { + const handleFilterData = (holdings: DAOHolding[]) => { + let data = holdings.slice() + if (filters?.token && filters.token !== "") { + data = holdings.filter(trx => trx.token?.symbol.toLowerCase() === filters.token?.toLocaleLowerCase()) + } + if (filters?.balanceMin && filters.balanceMin !== "") { + data = holdings.filter(trx => trx.balance.isGreaterThanOrEqualTo(filters.balanceMin!)) + } + if (filters?.balanceMax && filters.balanceMax !== "") { + data = holdings.filter(trx => trx.balance.isLessThanOrEqualTo(filters.balanceMax!)) + } + return data + } + if (!tokenHoldings) { return [] } + let holdings = tokenHoldings.slice() + const xtz: DAOHolding = { + token: { + symbol: "XTZ", + id: "XTZ", + contract: "", + token_id: 0, + name: "", + decimals: 6, + network: "mainnet", + supply: tezosBalance || new BigNumber(0), + standard: "" + }, + balance: tezosBalance || new BigNumber(0) + } + holdings.unshift(xtz) + + if (filters) { + holdings = handleFilterData(holdings) + } - return tokenHoldings.map(createData) - }, [tokenHoldings]) + if (searchText) { + holdings = holdings.filter( + holding => holding.token && holding.token.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + + return holdings.map(createData) + }, [tokenHoldings, searchText, filters, tezosBalance]) return ( <> + + setOpenFiltersDialog(true)} + xs={isSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + { defaultValues={defaultValues} defaultTab={0} /> + ) } diff --git a/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx b/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx index 6b598071..61b81cef 100644 --- a/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx +++ b/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx @@ -1,5 +1,5 @@ +import React, { useEffect, useMemo, useState } from "react" import dayjs from "dayjs" -import React, { useMemo, useState } from "react" import { Grid, Link, @@ -20,9 +20,10 @@ import { ContentContainer } from "modules/explorer/components/ContentContainer" import { networkNameMap } from "services/bakingBad" import { toShortAddress } from "services/contracts/utils" import { ReactComponent as BulletIcon } from "assets/img/bullet.svg" -import { CopyButton } from "modules/common/CopyButton" import ReactPaginate from "react-paginate" import "../../DAOList/styles.css" +import OpenInNewIcon from "@mui/icons-material/OpenInNew" +import numbro from "numbro" const localizedFormat = require("dayjs/plugin/localizedFormat") dayjs.extend(localizedFormat) @@ -33,7 +34,8 @@ const createData = (transfer: TransferWithBN) => { date: dayjs(transfer.date).format("L"), address: transfer.recipient, amount: transfer.amount.dp(10, 1).toString(), - hash: transfer.hash + hash: transfer.hash, + type: transfer.type } } @@ -65,16 +67,70 @@ const ProposalsFooterMobile = styled(Grid)({ borderBottomRightRadius: 8 }) +const ItemContainer = styled(Grid)(({ theme }) => ({ + padding: "40px 48px", + gap: 8, + borderRadius: 8, + background: theme.palette.primary.main, + [theme.breakpoints.down("sm")]: { + padding: "30px 38px", + gap: 20 + } +})) + +const Container = styled(Grid)({ + gap: 24, + display: "grid" +}) + +const Title = styled(Typography)({ + color: "#fff", + fontSize: 24 +}) + +const Subtitle = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light, + fontSize: 16, + fontWeight: 300 +})) + +const AmountText = styled(Typography)(({ theme }) => ({ + color: "#fff", + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" +})) + +const BlockExplorer = styled(Typography)({ + "fontSize": 16, + "fontWeight": 400, + "cursor": "pointer", + "display": "flex", + "alignItems": "center", + "& svg": { + fontSize: 16, + marginRight: 6 + } +}) + interface RowData { token: string date: string amount: string address: string hash: string + type: string | undefined } const Titles = ["Token", "Date", "Recipient", "Amount"] +const formatConfig = { + average: true, + mantissa: 1, + thousandSeparated: true, + trimMantissa: true +} + const titleDataMatcher = (title: (typeof Titles)[number], rowData: RowData) => { switch (title) { case "Token": @@ -122,6 +178,7 @@ const MobileTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ const newOffset = (event.selected * 5) % data.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(data ? data.length / 5 : 0) @@ -183,84 +240,93 @@ const MobileTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ ) } -const DesktopTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ data: rows, network }) => { +const TransfersTableItems: React.FC<{ data: RowData[]; network: Network }> = ({ data: rows, network }) => { const [currentPage, setCurrentPage] = useState(0) const [offset, setOffset] = useState(0) + const theme = useTheme() + const isSmall = useMediaQuery(theme.breakpoints.down("xs")) + + useEffect(() => { + setOffset(0) + }, [rows]) + + const openBlockExplorer = (hash: string) => { + window.open(`https://${networkNameMap[network]}.tzkt.io/` + hash, "_blank") + } + // Invoke when user click to request another page. const handlePageClick = (event: { selected: number }) => { if (rows) { const newOffset = (event.selected * 5) % rows.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(rows ? rows.length / 5 : 0) return ( <> - - - - {Titles.map((title, i) => ( - {title} - ))} - - - - {rows.slice(offset, offset + 5).map((row, i) => ( - - -
- {row.token} -
-
- {row.date} - - {toShortAddress(row.address)} - - - {row.amount} -
- ))} - {!(rows && rows.length > 0) ? ( - - - - No items - - - - ) : null} -
-
- - - + {rows && rows.length > 0 ? ( + <> + + {rows.slice(offset, offset + 5).map((row, i) => { + return ( + + + + + {row.token} + + + To {toShortAddress(row.address)} + {isSmall ? null : •} + {dayjs(row.date).format("ll")} + + + + + + {row.type ? (row.type === "Deposit" ? "-" : "+") : null}{" "} + {isSmall ? numbro(row.amount).format(formatConfig) : row.amount} {row.token}{" "} + + + + openBlockExplorer(row.hash)}> + + View on Block Explorer + + + + + ) + })} + + + + + + + ) : ( + No items + )} ) } export const TransfersTable: React.FC<{ transfers: TransferWithBN[] }> = ({ transfers }) => { - const theme = useTheme() - const isSmall = useMediaQuery(theme.breakpoints.down("sm")) - const rows = useMemo(() => { if (!transfers) { return [] @@ -273,10 +339,10 @@ export const TransfersTable: React.FC<{ transfers: TransferWithBN[] }> = ({ tran return ( - {isSmall ? ( - + {rows && rows.length > 0 ? ( + ) : ( - + No items )} ) diff --git a/src/modules/explorer/pages/Treasury/components/TreasuryDialog.tsx b/src/modules/explorer/pages/Treasury/components/TreasuryDialog.tsx index d17f33d9..d5fa860a 100644 --- a/src/modules/explorer/pages/Treasury/components/TreasuryDialog.tsx +++ b/src/modules/explorer/pages/Treasury/components/TreasuryDialog.tsx @@ -26,7 +26,7 @@ const OptionContainer = styled(Grid)(({ theme }) => ({ "padding": "35px 42px", "marginBottom": 16, "cursor": "pointer", - "height": 110, + "height": 80, "&:hover:enabled": { background: theme.palette.secondary.dark, scale: 1.01, diff --git a/src/modules/explorer/pages/Treasury/index.tsx b/src/modules/explorer/pages/Treasury/index.tsx index b9a92a6c..55a92428 100644 --- a/src/modules/explorer/pages/Treasury/index.tsx +++ b/src/modules/explorer/pages/Treasury/index.tsx @@ -1,12 +1,10 @@ -import { Button, Grid, Theme, Tooltip, Typography, useMediaQuery, useTheme } from "@material-ui/core" -import { ProposalFormContainer } from "modules/explorer/components/ProposalForm" - import React, { useMemo, useState } from "react" +import { Button, Grid, Theme, Tooltip, Typography, useMediaQuery, useTheme } from "@material-ui/core" import { useDAO } from "services/services/dao/hooks/useDAO" import { useDAOID } from "../DAO/router" import { BalancesTable } from "./components/BalancesTable" import { TransfersTable } from "./components/TransfersTable" -import { useTransfers } from "../../../../services/contracts/baseDAO/hooks/useTransfers" +import { TransferWithBN, useTransfers } from "../../../../services/contracts/baseDAO/hooks/useTransfers" import { InfoIcon } from "../../components/styled/InfoIcon" import { useIsProposalButtonDisabled } from "../../../../services/contracts/baseDAO/hooks/useCycleInfo" import { styled } from "@material-ui/core" @@ -19,6 +17,14 @@ import { SmallButton } from "modules/common/SmallButton" import { ContentContainer } from "modules/explorer/components/ContentContainer" import { CopyButton } from "modules/common/CopyButton" import { TreasuryDialog } from "./components/TreasuryDialog" +import { SearchInput } from "../DAOList/components/Searchbar" +import FilterAltIcon from "@mui/icons-material/FilterAlt" +import { FilterTransactionsDialog } from "modules/explorer/components/FiltersTransactionsDialog" +import { StatusOption } from "modules/explorer/components/FiltersUserDialog" + +const FiltersContainer = styled(Grid)({ + cursor: "pointer" +}) const ItemGrid = styled(Grid)({ width: "inherit" @@ -50,6 +56,19 @@ const TitleText = styled(Typography)({ } }) +export interface TransactionsFilters { + token: string | null + sender: string | null + receiver: string | null + status: StatusOption | undefined +} + +export interface TokensFilters { + token: string | null + balanceMin: string | undefined + balanceMax: string | undefined +} + const StyledTab = styled(Button)(({ theme, isSelected }: { theme: Theme; isSelected: boolean }) => ({ "fontSize": 18, "height": 40, @@ -89,6 +108,9 @@ export const Treasury: React.FC = () => { const { data: dao } = useDAO(daoId) const [openTransfer, setOpenTransfer] = useState(false) const [selectedTab, setSelectedTab] = React.useState(0) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) const { data: transfers } = useTransfers(daoId) @@ -101,6 +123,54 @@ export const Treasury: React.FC = () => { setOpenTransfer(false) } + const currentTransfers = useMemo(() => { + const handleFilterData = (allTransfers: TransferWithBN[]) => { + let data = allTransfers.slice() + if (filters?.receiver && filters.receiver !== "") { + data = allTransfers.filter(trx => trx.recipient === filters.receiver) + } + if (filters?.sender && filters.sender !== "") { + data = allTransfers.filter(trx => trx.sender === filters.sender) + } + if (filters?.token && filters.token !== "") { + data = allTransfers.filter(trx => trx.token?.symbol.toLocaleLowerCase() === filters.token?.toLocaleLowerCase()) + } + if (filters?.status && filters.status.label !== "") { + data = allTransfers.filter(trx => trx.status === filters.status?.label) + } + return data + } + + if (transfers) { + let allTransfers = transfers.slice() + if (filters) { + allTransfers = handleFilterData(allTransfers) + } + + if (searchText) { + return allTransfers.filter( + formattedDao => formattedDao.name && formattedDao.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + + return allTransfers + } + + return [] + }, [searchText, transfers, filters]) + + const filterByName = (filter: string) => { + setSearchText(filter.trim()) + } + + const handleFilters = (filters: TransactionsFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + return ( <> @@ -131,7 +201,7 @@ export const Treasury: React.FC = () => { justifyContent="flex-end" alignItems="center" style={{ gap: 15 }} - direction={isMobileSmall ? "column" : "row"} + direction={isMobileSmall ? "row" : "row"} xs={isMobileSmall ? undefined : true} > {dao && ( @@ -139,7 +209,7 @@ export const Treasury: React.FC = () => { item xs container - justifyContent="flex-end" + justifyContent={"flex-end"} direction="row" style={isMobileSmall ? {} : { marginLeft: 30 }} > @@ -175,7 +245,9 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } variant="contained" disableElevation={true} onClick={() => handleChangeTab(0)} @@ -186,7 +258,9 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } disableElevation={true} variant="contained" onClick={() => handleChangeTab(1)} @@ -197,13 +271,15 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } disableElevation={true} variant="contained" onClick={() => handleChangeTab(2)} isSelected={selectedTab === 2} > - History + Transactions @@ -219,7 +295,23 @@ export const Treasury: React.FC = () => { - + + setOpenFiltersDialog(true)} + xs={isMobileSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + + @@ -230,6 +322,12 @@ export const Treasury: React.FC = () => { open={openTransfer} handleClose={onCloseTransfer} /> + ) } diff --git a/src/modules/explorer/pages/User/components/UserMovements.tsx b/src/modules/explorer/pages/User/components/UserMovements.tsx index e49ec00e..27d30e07 100644 --- a/src/modules/explorer/pages/User/components/UserMovements.tsx +++ b/src/modules/explorer/pages/User/components/UserMovements.tsx @@ -302,8 +302,8 @@ export const UserMovements: React.FC<{ )} diff --git a/src/modules/explorer/pages/User/index.tsx b/src/modules/explorer/pages/User/index.tsx index 6340fbbd..6ed47657 100644 --- a/src/modules/explorer/pages/User/index.tsx +++ b/src/modules/explorer/pages/User/index.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, Theme, Typography, styled, useMediaQuery, useTheme } from "@material-ui/core" +import { Box, Button, Grid, Theme, Typography, styled, useMediaQuery, useTheme } from "@material-ui/core" import dayjs from "dayjs" import { useDAOID } from "modules/explorer/pages/DAO/router" import React, { useCallback, useEffect, useMemo, useState } from "react" @@ -15,12 +15,11 @@ import { StatusBadge } from "../../components/StatusBadge" import { ProfileAvatar } from "../../components/styled/ProfileAvatar" import { UserBalances } from "../../components/UserBalances" import { UserProfileName } from "../../components/UserProfileName" -import { usePolls } from "modules/lite/explorer/hooks/usePolls" + import { Delegation } from "./components/DelegationBanner" import { useTokenDelegationSupported } from "services/contracts/token/hooks/useTokenDelegationSupported" import { UserMovements } from "./components/UserMovements" -import { useUserVotes } from "modules/lite/explorer/hooks/useUserVotes" -import { Poll } from "models/Polls" + import { CopyButton } from "modules/explorer/components/CopyButton" const ContentBlockItem = styled(Grid)(({ theme }: { theme: Theme }) => ({ diff --git a/src/modules/explorer/utils/contract.ts b/src/modules/explorer/utils/contract.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/modules/lite/explorer/components/ChoiceItemSelected.tsx b/src/modules/lite/explorer/components/ChoiceItemSelected.tsx index 317a74b6..7a4546c7 100644 --- a/src/modules/lite/explorer/components/ChoiceItemSelected.tsx +++ b/src/modules/lite/explorer/components/ChoiceItemSelected.tsx @@ -3,18 +3,22 @@ import { Button, Divider, Grid, styled, Theme, Typography, useMediaQuery, useThe import { Choice } from "models/Choice" const StyledContainer = styled(Grid)(({ theme }: { theme: Theme }) => ({ - borderRadius: 4, + borderRadius: 8, minHeight: 75, - border: "1px solid", - borderColor: theme.palette.primary.light, - cursor: "pointer" + border: "none", + cursor: "pointer", + backgroundColor: theme.palette.primary.main })) +const Text = styled(Typography)({ + fontWeight: 300 +}) + const StyledButton = styled(Button)({ "width": "100%", "minHeight": "inherit", "&:hover": { - background: "rgba(129, 254, 183, 0.62)" + background: "#2d433c" } }) @@ -60,7 +64,7 @@ export const ChoiceItemSelected: React.FC<{ isPartOfVotes() ? { border: "1px solid", - borderColor: theme.palette.secondary.main, + borderColor: "#2D433C", backgroundColor: "#334d43" } : {} @@ -75,9 +79,7 @@ export const ChoiceItemSelected: React.FC<{ return }} > - - {choice.name} - + {choice.name} ) diff --git a/src/modules/lite/explorer/components/Choices.tsx b/src/modules/lite/explorer/components/Choices.tsx index 1e3fc047..126bd692 100644 --- a/src/modules/lite/explorer/components/Choices.tsx +++ b/src/modules/lite/explorer/components/Choices.tsx @@ -20,7 +20,6 @@ import { useDAOID } from "modules/explorer/pages/DAO/router" import { useDAO } from "services/services/dao/hooks/useDAO" import { useTokenVoteWeight } from "services/contracts/token/hooks/useTokenVoteWeight" import { useCommunity } from "../hooks/useCommunity" -import BigNumber from "bignumber.js" const ChoicesContainer = styled(Grid)(({ theme }) => ({ marginTop: 24, diff --git a/src/modules/lite/explorer/components/CreatorBadge.tsx b/src/modules/lite/explorer/components/CreatorBadge.tsx index 58d02a25..d2f60d6b 100644 --- a/src/modules/lite/explorer/components/CreatorBadge.tsx +++ b/src/modules/lite/explorer/components/CreatorBadge.tsx @@ -1,22 +1,18 @@ import React from "react" -import { Grid, Typography } from "@material-ui/core" +import { Grid, Typography, styled } from "@material-ui/core" import { Blockie } from "modules/common/Blockie" import { toShortAddress } from "services/contracts/utils" +const Text = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light +})) export const CreatorBadge: React.FC<{ address: string | undefined }> = ({ address }) => { return ( - - by - - - - + - - {toShortAddress(address || "")} - + {toShortAddress(address || "")} diff --git a/src/modules/lite/explorer/components/DownloadCsvFile.tsx b/src/modules/lite/explorer/components/DownloadCsvFile.tsx index ce11bd38..3b5e5400 100644 --- a/src/modules/lite/explorer/components/DownloadCsvFile.tsx +++ b/src/modules/lite/explorer/components/DownloadCsvFile.tsx @@ -4,36 +4,55 @@ import { ReactComponent as DownloadCSVIcon } from "assets/img/download_csv.svg" import { Choice } from "models/Choice" import { mkConfig, generateCsv, download } from "export-to-csv" import { useNotification } from "modules/lite/components/hooks/useNotification" +import BigNumber from "bignumber.js" +import { bytes2Char } from "@taquito/utils" +import dayjs from "dayjs" + +interface GroupedVotes { + address: string + options: any[] +} type DownloadCsvFileProps = { - data: Choice[] + data: GroupedVotes[] pollId: string | undefined symbol: string + decimals: string + isXTZ: boolean } -export const DownloadCsvFile: React.FC = ({ data, pollId, symbol }) => { +export const DownloadCsvFile: React.FC = ({ data, pollId, symbol, decimals, isXTZ }) => { const [votesDetails, setVotesDetails] = useState() const openNotification = useNotification() - useEffect(() => { + const getTotal = (options: any) => { + let total = new BigNumber(0) + if (options) { + options.map((item: any) => { + total = total.plus(new BigNumber(item.balance)) + }) + } + const formatted = total.div(new BigNumber(10).pow(isXTZ ? 6 : decimals)) + return formatted + } const arr: any = [] + data.map(item => { - item.walletAddresses.map(vote => { - const formattedVote = { - address: vote.address, - choice: item.name, - balance: vote.balanceAtReferenceBlock, - signature: vote.signature, - ipfsStorage: vote.cidLink - } - return arr.push(formattedVote) - }) + const formattedVote = { + address: item.address, + choices: item.options.map(item => item.name), + balance: getTotal(item.options), + signature: item.options[0].signature, + ipfsStorage: item.options[0].cidLink, + timestamp: + item.options[0] && item.options[0].payloadBytes ? bytes2Char(item.options[0].payloadBytes).split(" ")[4] : "" + } + return arr.push(formattedVote) }) setVotesDetails(arr) - }, [data]) + }, [data, decimals, isXTZ]) const downloadCvs = () => { - console.log(votesDetails) const csvConfig = mkConfig({ useKeysAsHeaders: true, filename: `proposal-${pollId}`, @@ -43,11 +62,12 @@ export const DownloadCsvFile: React.FC = ({ data, pollId, const votesData = votesDetails.map((row: any) => { return { "Address": row.address, - "Choice": row.choice, + "Choice": row.choices.toLocaleString().replace(",", ", "), "Token": symbol, "Vote Weight": row.balance, "Signature": row.signature, - "IPFS Storage Link": row.ipfsStorage + "IPFS Storage Link": row.ipfsStorage, + "Timestamp": dayjs(row.timestamp).format("LLL").toString() } }) try { diff --git a/src/modules/lite/explorer/components/ProposalDetailCard.tsx b/src/modules/lite/explorer/components/ProposalDetailCard.tsx index e520666e..3ca820ac 100644 --- a/src/modules/lite/explorer/components/ProposalDetailCard.tsx +++ b/src/modules/lite/explorer/components/ProposalDetailCard.tsx @@ -3,15 +3,26 @@ import { Grid, styled, Typography, Link, useTheme, useMediaQuery, Popover, withS import { GridContainer } from "modules/common/GridContainer" import { ProposalStatus, TableStatusBadge } from "./ProposalTableRowStatusBadge" import { CreatorBadge } from "./CreatorBadge" -import { FileCopyOutlined, MoreHoriz, ShareOutlined } from "@material-ui/icons" -import Share from "assets/img/share.svg" -import { CommunityBadge } from "./CommunityBadge" -import LinkIcon from "assets/img/link.svg" +import { FileCopyOutlined } from "@material-ui/icons" import { Poll } from "models/Polls" import dayjs from "dayjs" +import LinkIcon from "assets/img/link.svg" + import { useNotification } from "modules/common/hooks/useNotification" import ReactHtmlParser from "react-html-parser" +const Title = styled(Typography)({ + fontSize: 32, + fontWeight: 600 +}) + +const Subtitle = styled(Typography)(({ theme }) => ({ + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" /* 28.8px */, + color: theme.palette.primary.light +})) + const LogoItem = styled("img")(({ theme }) => ({ cursor: "pointer", [theme.breakpoints.down("sm")]: { @@ -21,8 +32,12 @@ const LogoItem = styled("img")(({ theme }) => ({ const TextContainer = styled(Typography)(({ theme }) => ({ display: "flex", + color: theme.palette.primary.light, alignItems: "center", gap: 10, + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" /* 28.8px */, marginRight: 8, [theme.breakpoints.down("sm")]: { marginTop: 20 @@ -32,6 +47,10 @@ const TextContainer = styled(Typography)(({ theme }) => ({ const EndTextContainer = styled(Typography)(({ theme }) => ({ display: "flex", alignItems: "center", + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" /* 28.8px */, + color: theme.palette.primary.light, gap: 10, marginRight: 8, [theme.breakpoints.down("sm")]: { @@ -40,12 +59,20 @@ const EndTextContainer = styled(Typography)(({ theme }) => ({ })) const EndText = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light, + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" /* 28.8px */, [theme.breakpoints.down("sm")]: { marginTop: 20 } })) const Divider = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light, + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" /* 28.8px */, marginLeft: 8, marginRight: 8, [theme.breakpoints.down("sm")]: { @@ -69,6 +96,12 @@ const CopyIcon = styled(FileCopyOutlined)({ cursor: "pointer" }) +const LinearContainer = styled(GridContainer)({ + background: "inherit !important", + backgroundColor: "inherit !important", + padding: "0px 0px 24px 0px" +}) + const CustomPopover = withStyles({ paper: { "marginTop": 10, @@ -81,6 +114,19 @@ const CustomPopover = withStyles({ } })(Popover) +const DescriptionContainer = styled(Grid)(({ theme }) => ({ + background: theme.palette.secondary.light, + padding: "40px 48px 42px 48px", + borderRadius: 8, + marginTop: 20, + gap: 32 +})) + +const DescriptionText = styled(Typography)({ + fontSize: 24, + fontWeight: 600 +}) + export const ProposalDetailCard: React.FC<{ poll: Poll | undefined; daoId: string }> = ({ poll, daoId }) => { const theme = useTheme() const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) @@ -111,8 +157,8 @@ export const ProposalDetailCard: React.FC<{ poll: Poll | undefined; daoId: strin return ( <> - - + + - - {poll?.name} - + {poll?.name} + + + - + - - - - Share - - - - - - Copy link - - + Off-Chain Proposal • Created by + + + - - - - - + + Created + {dayjs(Number(poll?.startTime)).format("LL")} + • + + {poll?.isActive === "closed" ? "Closed" : "End date"}{" "} + + {dayjs(Number(poll?.endTime)).format("ll")} + - - - - Start date:{" "} - - - {dayjs(Number(poll?.startTime)).format("lll")} - - - - - End date:{" "} - - - {dayjs(Number(poll?.endTime)).format("lll")} - - - @@ -205,6 +221,8 @@ export const ProposalDetailCard: React.FC<{ poll: Poll | undefined; daoId: strin + {ReactHtmlParser(poll?.description ? poll?.description : "")} + {poll?.externalLink ? ( @@ -214,7 +232,7 @@ export const ProposalDetailCard: React.FC<{ poll: Poll | undefined; daoId: strin ) : null} - + ) } diff --git a/src/modules/lite/explorer/components/VoteDetails.tsx b/src/modules/lite/explorer/components/VoteDetails.tsx index 0e6c387c..f4af8065 100644 --- a/src/modules/lite/explorer/components/VoteDetails.tsx +++ b/src/modules/lite/explorer/components/VoteDetails.tsx @@ -12,6 +12,7 @@ import { calculateWeight, calculateWeightXTZ, calculateXTZTotal, + getGroupedVotes, getTotalVoters, getTreasuryPercentage, nFormatter @@ -22,18 +23,33 @@ import { useCommunityToken } from "../hooks/useCommunityToken" import { getTurnoutValue } from "services/utils/utils" import { useTokenDelegationSupported } from "services/contracts/token/hooks/useTokenDelegationSupported" import { DownloadCsvFile } from "./DownloadCsvFile" +import { SmallButton } from "modules/common/SmallButton" + +const DescriptionText = styled(Typography)({ + fontSize: 24, + fontWeight: 600 +}) + +const TotalText = styled(Typography)({ + fontSize: 18, + fontWeight: 600 +}) + +const TotalValue = styled(Typography)({ + fontSize: 18, + fontWeight: 300 +}) const Container = styled(Grid)(({ theme }) => ({ - background: theme.palette.primary.main, + background: theme.palette.secondary.light, borderRadius: 8 })) const TitleContainer = styled(Grid)(({ theme }) => ({ - paddingTop: 18, - paddingLeft: 46, - paddingRight: 46, - paddingBottom: 18, - borderBottom: `0.3px solid ${theme.palette.primary.light}`, + padding: "40px 48px 10px", + borderRadius: 8, + marginTop: 20, + gap: 32, [theme.breakpoints.down("sm")]: { padding: "18px 25px" } @@ -41,12 +57,15 @@ const TitleContainer = styled(Grid)(({ theme }) => ({ const LinearContainer = styled(GridContainer)({ paddingBottom: 0, - minHeight: 110 + minHeight: 70, + background: "inherit !important" }) const LegendContainer = styled(GridContainer)({ minHeight: 30, - paddingBottom: 0 + paddingBottom: 0, + alignItems: "center", + background: "inherit" }) const GraphicsContainer = styled(Grid)({ @@ -70,6 +89,7 @@ export const VoteDetails: React.FC<{ const tokenData = useCommunityToken(communityId) const { data: isTokenDelegationSupported } = useTokenDelegationSupported(tokenData?.tokenAddress) const totalXTZ = calculateXTZTotal(choices) + const groupedVotes = getGroupedVotes(choices) const handleClickOpen = () => { setVotes(choices.filter(elem => elem.walletAddresses.length > 0)) @@ -105,9 +125,7 @@ export const VoteDetails: React.FC<{ return ( - - Results - + Votes {choices && @@ -175,12 +193,12 @@ export const VoteDetails: React.FC<{ - handleClickOpen()}> - {getTotalVoters(choices)} - - handleClickOpen()}> - Votes - + handleClickOpen()}> + Total Votes: + + + {numbro(calculateProposalTotal(choices, isXTZ ? 6 : tokenData?.decimals)).format(formatConfig)} + {isTokenDelegationSupported && turnout && !poll?.isXTZ ? ( ({turnout.toFixed(2)} % Turnout) @@ -197,13 +215,10 @@ export const VoteDetails: React.FC<{ sm={6} lg={6} style={{ gap: 10 }} - alignItems="baseline" + alignItems="center" justifyContent={isMobileSmall ? "flex-start" : "flex-end"} > - - {numbro(calculateProposalTotal(choices, isXTZ ? 6 : tokenData?.decimals)).format(formatConfig)} - - + {/* {isXTZ ? "XTZ" : poll?.tokenSymbol} {!poll?.isXTZ && ( @@ -218,20 +233,25 @@ export const VoteDetails: React.FC<{ .toString()} % of Total Supply) - )} + )} */} {getTotalVoters(choices) > 0 ? ( ) : null} + + setOpen(true)}> View Votes ({ padding: 0, @@ -41,41 +51,84 @@ const CopyIcon = styled(FileCopyOutlined)({ cursor: "pointer" }) -const Row = styled(Grid)(({ theme }) => ({ - "background": theme.palette.primary.main, - "padding": "24px 48px", - "paddingBottom": "0px", - "borderBottom": "0.3px solid #7D8C8B", - "&:last-child": { - borderRadius: "0px 0px 8px 8px", - borderBottom: "none" - }, - "&:first-child": { - borderRadius: "8px 8px 0px 0px" +const StyledTableCell = styled(TableCell)({ + "fontWeight": 300, + "& p": { + fontWeight: 300 + } +}) +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover }, + // hide last border + "&:last-child td, &:last-child th": { + border: 0 + } +})) + +const VotesRow = styled(Typography)(({ theme }) => ({ + cursor: "default", + display: "block", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + padding: ".5rem 1rem", + width: 400, [theme.breakpoints.down("sm")]: { - padding: "12px 24px" + width: 200, + textAlign: "center" } })) +const AddressText = styled(Typography)({ + marginLeft: 8 +}) + +const Header = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light, + fontWeight: 300 +})) + +const Container = styled(Grid)({ + gap: 16, + padding: 16 +}) + const formatConfig = { - mantissa: 4, - trimMantissa: true + mantissa: 2, + trimMantissa: false, + average: true, + thousandSeparated: true +} + +interface Votes { + address: string + options: VoteDetail[] +} + +interface VoteDetail { + name: string + balance: string } export const VotesDialog: React.FC<{ open: boolean handleClose: any choices: Choice[] + groupedVotes: Votes[] symbol: string - decimals: string + decimals: string | undefined isXTZ: boolean -}> = ({ open, handleClose, choices, symbol, decimals, isXTZ }) => { +}> = ({ open, handleClose, choices, symbol, decimals, isXTZ, groupedVotes }) => { const descriptionElementRef = React.useRef(null) const openNotification = useNotification() const theme = useTheme() const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) + const [currentPage, setCurrentPage] = useState(0) + const [offset, setOffset] = useState(0) + const pageCount = Math.ceil(groupedVotes ? groupedVotes.length / 4 : 0) React.useEffect(() => { if (open) { @@ -95,6 +148,49 @@ export const VotesDialog: React.FC<{ }) } + const listOfVotes = useMemo(() => { + const getTotal = (options: VoteDetail[]) => { + let total = new BigNumber(0) + if (options) { + options.map(item => { + total = total.plus(new BigNumber(item.balance)) + }) + } + const formatted = total.div(new BigNumber(10).pow(isXTZ ? 6 : decimals ? decimals : 0)) + return formatted + } + const array: any = [] + groupedVotes.map(item => { + const obj = { + address: item.address, + options: item.options.map(item => item.name), + total: getTotal(item.options), + rowref: React.createRef(), + open: false + } + return array.push(obj) + }) + + return array + }, [groupedVotes, decimals, isXTZ]) + + useEffect(() => { + document.querySelectorAll(".test").forEach((span: HTMLSpanElement) => { + if (span.scrollWidth > span.clientWidth) { + span.classList.add("ellipse") + } + }) + }, [groupedVotes, open]) + + // Invoke when user click to request another page. + const handlePageClick = (event: { selected: number }) => { + if (groupedVotes) { + const newOffset = (event.selected * 4) % groupedVotes.length + setOffset(newOffset) + setCurrentPage(event.selected) + } + } + return (
- {choices.map((elem: Choice, index: number) => { - { - return elem.walletAddresses.map((choice, num) => { - return ( - - - {toShortAddress(choice.address)} - copyAddress(choice.address)} color="secondary" fontSize="inherit" /> - - - - {" "} - {elem.name}{" "} - - - - - {" "} - {!isXTZ - ? formatByDecimals(choice.balanceAtReferenceBlock, decimals) - : numbro(formatByDecimals(choice.balanceAtReferenceBlock, "6")).format(formatConfig)}{" "} - {symbol}{" "} - - - - ) - }) - } - })} + + {!isMobileSmall ? ( + + + Address + Option + Votes + + + ) : null} + + + {listOfVotes + ? listOfVotes.map((row: any) => { + return isMobileSmall ? ( + + + +
Address
+
+ + + {toShortAddress(row.address)} + copyAddress(row.address)} color="secondary" fontSize="inherit" /> + +
{" "} + + +
Option
+
+ + + {" "} + {row.options.toLocaleString().replace(",", ", ")} + + +
{" "} + + +
Votes
+
+ + + {numbro(row.total).format(formatConfig)} {symbol}{" "} + + +
{" "} +
+ ) : ( + + + + + {toShortAddress(row.address)} + copyAddress(row.address)} color="secondary" fontSize="inherit" /> + + {" "} + + + {" "} + {row.options.toLocaleString().replace(",", ", ")} + + + + {" "} + + {numbro(row.total).format(formatConfig)} {symbol}{" "} + + + + ) + }) + : null} + {!listOfVotes ? No info : null} +
+
+ + +
- + {/* - + */}
) diff --git a/src/modules/lite/explorer/components/styles.css b/src/modules/lite/explorer/components/styles.css new file mode 100644 index 00000000..f2460490 --- /dev/null +++ b/src/modules/lite/explorer/components/styles.css @@ -0,0 +1,42 @@ +.overflow { + display: inline-block; + width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + height: 43px; +} + +.ellipse { + width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0; + padding: 0; + background: "inherit"; + transition: color 1s ease-in-out; + +} +.content li { + margin: 0; + padding: 0; + overflow: hidden; +} +.ellipse:hover { + border-radius: 8px; + padding: 16px; + box-shadow: 0px 3px 12px 2px #1c1f23; + white-space: normal; + word-break: break-word; + z-index: 5; + width: 400px; + position: absolute; + background: #383e42; + margin-top: -12px; + margin-left: 40px; + @media only screen and (max-width: 600px) { + width: 200px; + } + +} diff --git a/src/modules/lite/explorer/pages/ProposalDetails/index.tsx b/src/modules/lite/explorer/pages/ProposalDetails/index.tsx index 50cb8eb3..04f08a7f 100644 --- a/src/modules/lite/explorer/pages/ProposalDetails/index.tsx +++ b/src/modules/lite/explorer/pages/ProposalDetails/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react" -import { Button, Grid, Typography, styled, useMediaQuery, useTheme } from "@material-ui/core" +import { CircularProgress, Grid, Typography, styled, useMediaQuery, useTheme } from "@material-ui/core" import { ProposalDetailCard } from "../../components/ProposalDetailCard" import { GridContainer } from "modules/common/GridContainer" import { ChoiceItemSelected } from "../../components/ChoiceItemSelected" @@ -22,6 +22,12 @@ import { ArrowBackIosOutlined } from "@material-ui/icons" import { useIsMember } from "../../hooks/useIsMember" import { useHistoryLength } from "modules/explorer/context/HistoryLength" import { getEthSignature } from "services/utils/utils" +import { SmallButton } from "modules/common/SmallButton" + +const DescriptionText = styled(Typography)({ + fontSize: 24, + fontWeight: 600 +}) const PageContainer = styled("div")({ marginBottom: 50, @@ -49,6 +55,11 @@ const PageContainer = styled("div")({ } }) +const LinearContainer = styled(GridContainer)(({ theme }) => ({ + background: theme.palette.secondary.light, + borderRadius: 8 +})) + export const ProposalDetails: React.FC<{ id: string }> = ({ id }) => { const { proposalId } = useParams<{ proposalId: string @@ -77,6 +88,7 @@ export const ProposalDetails: React.FC<{ id: string }> = ({ id }) => { const [selectedVotes, setSelectedVotes] = useState([]) const isMember = useIsMember(network, community?.tokenAddress || "", account) + const [isLoading, setIsLoading] = useState(false) const navigateToDao = () => { if (historyLength > 1) { @@ -147,6 +159,7 @@ export const ProposalDetails: React.FC<{ id: string }> = ({ id }) => { autoHideDuration: 3000, variant: "error" }) + setIsLoading(false) return } } else if (etherlink.isConnected) { @@ -206,48 +219,58 @@ export const ProposalDetails: React.FC<{ id: string }> = ({ id }) => { Back to community
- + {choices && choices.length > 0 ? ( - - - {choices.map((choice, index) => { - return ( - - ) - })} - - {poll?.isActive === ProposalStatus.ACTIVE ? ( - - ) : null} - + {choices.map((choice, index) => { + return ( + + ) + })} + + {poll?.isActive === ProposalStatus.ACTIVE ? ( + !isLoading ? ( + saveVote()} + style={{ marginTop: 20 }} + > + {votingPower && votingPower.gt(new BigNumber(0)) ? "Cast your vote" : "No Voting Weight"} + + ) : ( + + ) + ) : null} + + ) : null} - + {poll && poll !== undefined ? ( , name?: string): [token, number] { + switch (token.__michelsonType) { + case "bls12_381_fr": + case "bls12_381_g1": + case "bls12_381_g2": + case "chain_id": + case "key_hash": + case "key": + case "bytes": + case "signature": + case "string": + initTokenTable(init, counter) + return [ + { + counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + initValue: "" + }, + counter + ] + case "address": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value) { + if (validateAddress(value) !== 3) { + return `invalid address ${value}` + } + }, + initValue: "" + }, + counter + ] + case "contract": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: "contract", + validate(value) { + if (validateAddress(value) !== 3) { + return `invalid address ${value}` + } + }, + initValue: "" + }, + counter + ] + case "bool": + initTokenTable(init, counter, false) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + initValue: false + }, + counter + ] + case "int": + case "nat": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value) { + if (value && isNaN(Number(value))) { + return `Invalid number, got: ${value}` + } + }, + initValue: "" + }, + counter + ] + case "mutez": + case "timestamp": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value) { + const n = Number(value) + if (isNaN(n)) { + return `Invalid number, got: ${value}` + } + if (n < 0) { + return `Number should be greater or equal to 0, got ${value}` + } + }, + initValue: "" + }, + counter + ] + case "never": + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + case "operation": + throw new Error("can't happen: operation is forbidden in the parameter") + case "chest": + case "chest_key": + throw new Error( + "can't happen(Tezos bug): time lock related instructions is disabled in the client because of a vulnerability" + ) + case "unit": + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + case "or": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, schemas[0][0]) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: schemas[0][0] + }, + new_counter + ] + } + case "set": + case "list": { + initTokenTable(init, counter, []) + const [child, new_counter] = parseSchema(counter + 1, token.schema, init) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [child], + initValue: [] + }, + new_counter + ] + } + case "pair": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, []) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: [] + }, + new_counter + ] + } + case "map": + case "big_map": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, []) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: [] + }, + new_counter + ] + } + case "option": { + const [child, new_counter] = parseSchema(counter + 1, token.schema, init) + + initTokenTable(init, counter, "none") + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [child], + initValue: "none" + }, + new_counter + ] + } + case "constant": + throw new Error("can't happen: constant will never be in parameter") + case "lambda": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + placeholder: "lambda", + children: [], + initValue: "" + }, + counter + ] + case "sapling_transaction_deprecated": + case "sapling_transaction": + case "sapling_state": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + placeholder: token.__michelsonType + " " + token.schema.memoSize, + children: [], + initValue: "" + }, + counter + ] + case "ticket_deprecated": + case "ticket": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + default: + return assertNever(token as never) + } +} + +async function parseContractScript(c: any, initTokenTable: Record) { + let token, + counter = 0 + const entryponts = Object.entries(c.entrypoints.entrypoints).reverse() + if (entryponts.length == 0) { + ;[token, counter] = parseSchema(0, c.parameterSchema.generateSchema(), initTokenTable, "entrypoint") + console.log("Token:", token) + } else { + console.log("Case 2") + // handle the case of multiple entrypoints + const childrenToken = [] + let childToken + let init + let setInit = false + for (let i = 0; i < entryponts.length; i++) { + const [entrypoint, type] = entryponts[i] + const schema = new Schema(type as MichelineMichelsonV1Expression).generateSchema() + if (schema.__michelsonType !== "or") { + if (!setInit) { + init = entrypoint + setInit = true + } + let new_counter + ;[childToken, new_counter] = parseSchema(counter, schema, initTokenTable, entrypoint) + counter = new_counter + 1 + childrenToken.push(childToken) + } + } + counter = counter + 1 + if (typeof init === "undefined") throw new Error("internal error: initial entrypoint is undefined") + token = { + counter, + name: "entrypoint", + type: "or", + children: childrenToken, + initValue: init + } + initTokenTable[getFieldName(token.counter)] = token.initValue + } + initTokenTable["counter"] = counter + return token +} + +async function getContractEndpoints(network: string, contractAddress: string) { + try { + const tezosNetwork = network === "ghostnet" ? "ghostnet" : "mainnet" + const tezos = new TezosToolkit(rpcNodes[tezosNetwork]) + const contract = await tezos.contract.at(contractAddress) + const endpoints = await parseContractScript(contract, {}) + console.log("Endpoints:", endpoints) + return [endpoints, null] + } catch (error) { + console.error("Error fetching contract:", error) + return [null, error] + } +} + +export { getContractEndpoints } diff --git a/src/services/aci/index.ts b/src/services/aci/index.ts new file mode 100644 index 00000000..fab161d8 --- /dev/null +++ b/src/services/aci/index.ts @@ -0,0 +1,709 @@ +import { emitMicheline, Parser } from "@taquito/michel-codec" +import { MichelsonMap } from "@taquito/taquito" +import { validateAddress } from "@taquito/utils" + +import { assertNever } from "assert-never" +import { BigNumber } from "bignumber.js" + +import { Schema } from "@taquito/michelson-encoder" +import type { MichelineMichelsonV1Expression } from "@airgap/beacon-sdk" +import type { TokenSchema } from "@taquito/michelson-encoder" + +type version = "1.0.0" | "unknown version" + +type michelsonType = + | "address" + | "bool" + | "bytes" + | "int" + | "key" + | "key_hash" + | "mutez" + | "nat" + | "string" + | "timestamp" + | "bls12_381_fr" + | "bls12_381_g1" + | "bls12_381_g2" + | "chain_id" + | "never" + | "operation" + | "chest" + | "chest_key" + | "signature" + | "unit" + | "tx_rollup_l2_address" + | "or" + | "pair" + | "list" + | "set" + | "option" + | "map" + | "big_map" + | "constant" + | "contract" + | "lambda" + | "sapling_state" + | "sapling_transaction" + | "sapling_transaction_deprecated" + | "ticket" + | "ticket_deprecated" + +export type tokenMap = Record<"key" | "value", token> + +export type tokenValueType = string | boolean | number | token | token[] | tokenMap[] + +export type token = { + counter: number + name?: string + type: michelsonType + children: token[] + placeholder?: string + validate?: (value: string) => string | undefined + initValue: tokenValueType +} + +export type makeContractExecutionParam = { + address: string + entrypoint: string + type: string + amount: number + param: string +} + +function generateExecuteContractMichelson( + version: version, + { address, entrypoint, type, amount, param }: makeContractExecutionParam +) { + let michelsonEntrypoint = "" + if (entrypoint !== "default") { + michelsonEntrypoint = `%${entrypoint}` + } + console.log("Lambda Generate", { address, entrypoint, type, amount, param }) + if (version === "1.0.0") { + return `{ + NIL operation ; + PUSH address "${address}"; + CONTRACT ${michelsonEntrypoint} ${type}; + IF_NONE { PUSH string "UNKNOWN ADDRESS"; FAILWITH } { }; + PUSH mutez ${amount}; + PUSH ${type} ${param} ; + TRANSFER_TOKENS ; + CONS ; + SWAP; + CAR; + CAR; + NONE address; + PAIR; + PAIR; + }` + } else if (version !== "unknown version") { + return `{ + DROP; + PUSH address "${address}"; + CONTRACT ${michelsonEntrypoint} ${type}; + IF_NONE { PUSH string "contract dosen't exist"; FAILWITH } { }; + PUSH mutez ${amount}; + PUSH ${type} ${param} ; + TRANSFER_TOKENS ; + }` + } + // if (version === "1.0.0") { + // return `{ + // DROP; + // NIL operation ; + // PUSH address "${address}"; + // CONTRACT ${michelsonEntrypoint} ${type}; + // IF_NONE { PUSH string "contract dosen't exist"; FAILWITH } { }; + // PUSH mutez ${amount}; + // PUSH ${type} ${param} ; + // TRANSFER_TOKENS ; + // CONS ; + // }` + // } else if (version !== "unknown version") { + // return `{ + // DROP; + // PUSH address "${address}"; + // CONTRACT ${michelsonEntrypoint} ${type}; + // IF_NONE { PUSH string "contract dosen't exist"; FAILWITH } { }; + // PUSH mutez ${amount}; + // PUSH ${type} ${param} ; + // TRANSFER_TOKENS ; + // }` + // } + + throw new Error("Can't generate for an unknow version") +} + +function showName(type: string, name?: string) { + if (name && isNaN(Number(name))) { + return `${name} : ${type}` + } else { + return type + } +} + +function getFieldName(id: number): string { + return `input-${id.toString()}` +} + +function initTokenTable(init: Record, counter: number, defaultInit: tokenValueType = ""): void { + init[getFieldName(counter)] = defaultInit +} + +function parseSchema( + counter: number, + token: TokenSchema | any, + init: Record, + name?: string +): [token, number] { + switch (token.__michelsonType) { + case "bls12_381_fr": + case "bls12_381_g1": + case "bls12_381_g2": + case "chain_id": + case "key_hash": + case "key": + case "bytes": + case "signature": + case "string": + initTokenTable(init, counter) + return [ + { + counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + initValue: "" + }, + counter + ] + case "address": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value: string): string | undefined { + if (validateAddress(value) !== 3) { + return `invalid address ${value}` + } + }, + initValue: "" + }, + counter + ] + case "contract": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: "contract", + validate(value: string): string | undefined { + if (validateAddress(value) !== 3) { + return `invalid address ${value}` + } + }, + initValue: "" + }, + counter + ] + case "bool": + initTokenTable(init, counter, false) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + initValue: false + }, + counter + ] + case "int": + case "nat": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value: string): string | undefined { + if (value && isNaN(Number(value))) { + return `Invalid number, got: ${value}` + } + }, + initValue: "" + }, + counter + ] + case "mutez": + case "timestamp": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + placeholder: token.__michelsonType, + validate(value: string): string | undefined { + const n = Number(value) + if (isNaN(n)) { + return `Invalid number, got: ${value}` + } + if (n < 0) { + return `Number should be greater or equal to 0, got ${value}` + } + }, + initValue: "" + }, + counter + ] + case "never": + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + case "operation": + throw new Error("can't happen: operation is forbidden in the parameter") + case "chest": + case "chest_key": + throw new Error( + "can't happen(Tezos bug): time lock related instructions is disabled in the client because of a vulnerability" + ) + case "unit": + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + case "tx_rollup_l2_address": + throw new Error("can't happen: this type has been disable") + case "or": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child: token + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, schemas[0][0]) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: schemas[0][0] + }, + new_counter + ] + } + case "set": + case "list": { + initTokenTable(init, counter, [] as token[]) + const [child, new_counter] = parseSchema(counter + 1, token.schema, init) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [child], + initValue: [] as token[] + }, + new_counter + ] + } + case "pair": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child: token + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, []) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: [] + }, + new_counter + ] + } + case "map": + case "big_map": { + const schemas = Object.entries(token.schema) + let new_counter = counter + const children: token[] = [] + let child: token + schemas.forEach(([k, v]) => { + ;[child, new_counter] = parseSchema(new_counter + 1, v, init, k) + children.push(child) + }) + initTokenTable(init, counter, []) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children, + initValue: [] + }, + new_counter + ] + } + case "option": { + const [child, new_counter] = parseSchema(counter + 1, token.schema, init) + + initTokenTable(init, counter, "none") + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [child], + initValue: "none" + }, + new_counter + ] + } + case "constant": + throw new Error("can't happen: constant will never be in parameter") + case "lambda": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + placeholder: "lambda", + children: [], + initValue: "" + }, + counter + ] + case "sapling_transaction_deprecated": + case "sapling_transaction": + case "sapling_state": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + placeholder: token.__michelsonType + " " + token.schema.memoSize, + children: [], + initValue: "" + }, + counter + ] + case "ticket_deprecated": + case "ticket": + initTokenTable(init, counter) + return [ + { + counter: counter, + name, + type: token.__michelsonType, + children: [], + initValue: "" + }, + counter + ] + default: + return assertNever(token.__michelsonType as never) + } +} +function evalTaquitoParam(token: token, tableValue: Record): any { + switch (token.type) { + case "bls12_381_fr": + case "bls12_381_g1": + case "bls12_381_g2": + case "chain_id": + case "key_hash": + case "key": + case "bytes": + case "address": + case "signature": + case "string": + case "contract": + return tableValue[getFieldName(token.counter)] + case "bool": + return tableValue[getFieldName(token.counter)] + case "int": + case "nat": + case "mutez": + case "timestamp": { + const value = tableValue[getFieldName(token.counter)] + if (typeof value !== "string") + throw new Error(`The value get from UI should be in string, ${showName(token.type, token.name)}`) + if (!value) { + throw new Error(`Incorrect or empty value, ${showName(token.type, token.name)}`) + } + return new BigNumber(value) + } + case "never": + return undefined + case "operation": + throw new Error("can't happen: operation is forbidden in the parameter") + case "chest": + case "chest_key": + throw new Error( + "can't happen(Tezos bug): time lock related instructions is disabled in the client because of a vulnerability" + ) + case "unit": + return [["unit"]] + case "tx_rollup_l2_address": + throw new Error("can't happen: this type has been disabled") + case "or": { + const key = tableValue[getFieldName(token.counter)] + const child = key && token.children.find(x => x.name == key) + if (!child) { + throw new Error(`the selection ${key} doesn't exist`) + } + const value = evalTaquitoParam(child, tableValue) + return Object.fromEntries([[key, value]]) + } + case "set": + case "list": { + const values = tableValue[getFieldName(token.counter)] + if (!Array.isArray(values)) { + throw new Error(`internal error: the expected type of list or set is incorrect.`) + } + return values + .map(v => { + if ("counter" in v) { + return evalTaquitoParam(v, tableValue) + } else { + throw new Error(`internal error: the expected type of element of list or set is incorrect.`) + } + }) + .filter(v => v !== undefined) + } + case "pair": { + const raw: token[] = token.children + const values = raw.map((v, idx) => { + const check_key = isNaN(Number(v.name)) + return [check_key ? v.name : idx, evalTaquitoParam(v, tableValue)] + }) + return Object.fromEntries(values) as object + } + case "map": + case "big_map": { + const values = tableValue[getFieldName(token.counter)] + if (!Array.isArray(values)) { + throw new Error(`internal error: the expected type of map is incorrect.`) + } + const map = new MichelsonMap() + values.map(v => { + if ("counter" in v) { + throw new Error(`internal error: the expected type of element of list or set is incorrect.`) + } else { + map.set(evalTaquitoParam(v.key, tableValue), evalTaquitoParam(v.value, tableValue)) + } + }) + return map + } + case "option": { + const values = tableValue[getFieldName(token.counter)] + if (typeof values !== "string") { + throw new Error(`internal error: the expected value of option is incorrect.`) + } + if (values === "some") { + return evalTaquitoParam(token.children[0], tableValue) + } else { + return null + } + } + case "constant": + throw new Error("can't happen: constant will never be in parameter") + case "lambda": { + const values = tableValue[getFieldName(token.counter)] + if (typeof values !== "string") { + throw new Error(`internal error: the expected value of lambda is incorrect.`) + } + const p = new Parser() + return p.parseMichelineExpression(values) + } + case "sapling_transaction_deprecated": + case "sapling_transaction": + case "sapling_state": + return tableValue[getFieldName(token.counter)] + case "ticket_deprecated": + case "ticket": + return tableValue[getFieldName(token.counter)] + default: + return assertNever(token.type) + } +} + +function genLambda( + version: version, + props: { + address: string + amount: number + shape: any + reset: () => void + setField: (lambda: string, metadata: string) => void + setLoading: (x: boolean) => void + setState: (shape: any) => void + loading: boolean + }, + values: any +) { + let entrypoint = "default" + let taquitoParam + + const taquitoFullParam = evalTaquitoParam(props.shape.token, values) + console.log("token", props.shape.token, values, taquitoFullParam) + console.log("props shape", props.shape) + if (props.shape.contract.parameterSchema.isMultipleEntryPoint) { + const p = Object.entries(taquitoFullParam) + if (p.length !== 1) { + throw new Error("should only one entrypoint is selected") + } + ;[entrypoint, taquitoParam] = p[0] + } else { + taquitoParam = taquitoFullParam + } + const param = emitMicheline( + props.shape.contract.methodsObject[entrypoint](taquitoParam).toTransferParams().parameter.value + ) + + const micheline_type = props.shape.contract.parameterSchema.isMultipleEntryPoint + ? props.shape.contract.entrypoints.entrypoints[entrypoint] + : props.shape.contract.parameterSchema.root.val + const p = new Parser() + const type = emitMicheline(p.parseJSON(micheline_type), { + indent: "", + newline: "" + }) + + // This functione executes on client side + const lambda = generateExecuteContractMichelson(version, { + address: props.address, + entrypoint, + type, + amount: props.amount, + param + }) + console.log({ lambda }) + + props.setField( + lambda, + JSON.stringify( + { + contract_addr: props.address, + mutez_amount: props.amount, + entrypoint, + payload: param + }, + null, + 2 + ) + ) + props.setLoading(false) +} + +function allocateNewTokenCounter( + token: token, + counter: number, + setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void +): number { + let new_counter = counter + token.children.forEach((v, i) => { + new_counter = allocateNewTokenCounter(v, new_counter, setFieldValue) + new_counter = new_counter + 1 + v.counter = new_counter + setFieldValue(getFieldName(v.counter), v.initValue) + }) + return new_counter +} + +function parseContract(c: any, initTokenTable: Record) { + let token: token + let counter = 0 + // reverse the elements so the order of entrypoints will be in alphabet + const entryponts = Object.entries(c.entrypoints.entrypoints).reverse() + if (entryponts.length == 0) { + // handle the case of only "default" entrypoint + ;[token, counter] = parseSchema(0, c.parameterSchema.generateSchema(), initTokenTable, "entrypoint") + } else { + // handle the case of multiple entrypoints + const childrenToken: token[] = [] + let childToken + let init + let setInit = false + for (let i = 0; i < entryponts.length; i++) { + const [entrypoint, type] = entryponts[i] + const schema = new Schema(type as MichelineMichelsonV1Expression).generateSchema() + /** If the michelson type is "or", it means it's a nested entrypoint. + * The entrypoint is repeated. Therefore, don't make it as a child. + */ + if (schema.__michelsonType !== "or") { + /** + * Chose default value for selection component. + * Pick up the first non-nested entrypoint. + */ + if (!setInit) { + init = entrypoint + setInit = true + } + let new_counter + ;[childToken, new_counter] = parseSchema(counter, schema, initTokenTable, entrypoint) + counter = new_counter + 1 + childrenToken.push(childToken) + } + } + counter = counter + 1 + if (typeof init === "undefined") throw new Error("internal error: initial entrypoint is undefined") + token = { + counter, + name: "entrypoint", + type: "or", + children: childrenToken, + initValue: init + } + initTokenTable[getFieldName(token.counter)] = token.initValue + } + initTokenTable["counter"] = counter + return token +} + +export { + getFieldName, + evalTaquitoParam, + generateExecuteContractMichelson, + parseContract, + genLambda, + showName, + allocateNewTokenCounter +} diff --git a/src/services/aci/useArbitratyContractData.ts b/src/services/aci/useArbitratyContractData.ts new file mode 100644 index 00000000..f2f2ffb8 --- /dev/null +++ b/src/services/aci/useArbitratyContractData.ts @@ -0,0 +1,68 @@ +import { useMutation, useQueryClient } from "react-query" +import { useNotification } from "modules/common/hooks/useNotification" +import { useTezos } from "services/beacon/hooks/useTezos" +import mixpanel from "mixpanel-browser" +import { Network } from "services/beacon" +import { EnvKey, getEnv } from "services/config" +import { TezosToolkit } from "@taquito/taquito" + +export const useArbitraryContractData = () => { + const queryClient = useQueryClient() + const openNotification = useNotification() + const { network, tezos, account, connect } = useTezos() + + return useMutation< + any | Error, + Error, + { + contract: string + network: Network + handleContinue: () => void + finishLoad: (status: boolean) => void + showHeader: (status: boolean) => void + } + >( + async ({ contract, network, handleContinue, finishLoad, showHeader }) => { + try { + let tezosToolkit = tezos + + if (!account) { + tezosToolkit = (await connect()) as TezosToolkit + } + + const resp = await fetch(`${getEnv(EnvKey.REACT_APP_LITE_API_URL)}/aci/${contract}`, { + method: "POST", + body: JSON.stringify({ network: network }), + headers: { "Content-Type": "application/json" } + }) + + const data = await resp.json() + finishLoad(false) + if (data.success === false) { + openNotification({ + message: "Invalid contract address with unsupported prefix.", + variant: "error", + autoHideDuration: 10000 + }) + } else { + handleContinue() + showHeader(false) + } + return data + } catch (e) { + console.log(e) + openNotification({ + message: "Contract's data could not be fetch!", + variant: "error", + autoHideDuration: 10000 + }) + return new Error((e as Error).message) + } + }, + { + onSuccess: () => { + queryClient.resetQueries() + } + } + ) +} diff --git a/src/services/contracts/baseDAO/class.ts b/src/services/contracts/baseDAO/class.ts index e8549147..3688ee01 100644 --- a/src/services/contracts/baseDAO/class.ts +++ b/src/services/contracts/baseDAO/class.ts @@ -10,7 +10,7 @@ import { formatUnits, xtzToMutez } from "../utils" import { BigNumber } from "bignumber.js" import { Token } from "models/Token" import { Ledger } from "services/services/types" -import { Expr, Parser, packDataBytes, MichelsonType, MichelsonData } from "@taquito/michel-codec" +import { Expr, Parser, packDataBytes, MichelsonType, MichelsonData, emitMicheline } from "@taquito/michel-codec" import { Schema } from "@taquito/michelson-encoder" import configuration_type_michelson from "./lambdaDAO/michelson/supported_lambda_types/configuration_proposal_type.json" @@ -279,6 +279,20 @@ export abstract class BaseDAO { return packed } + // TODO: To be Implemented + public async proposeAciExecution(tezos: TezosToolkit, micheline_type: any) { + const contract = await getContract(tezos, this.data.address) + const p = new Parser() + + const type = emitMicheline(p.parseJSON(micheline_type), { + indent: "", + newline: "" + }) + + // const lambda_schema = p.parseMichelineExpression(aciLambda) as MichelsonType + // const lambda_schema = new Schema(lambda_schema) + } + public async proposeConfigChange(configParams: ConfigProposalParams, tezos: TezosToolkit) { const contract = await getContract(tezos, this.data.address) const p = new Parser() diff --git a/src/services/contracts/baseDAO/hooks/useLambdaExecutePropose.ts b/src/services/contracts/baseDAO/hooks/useLambdaExecutePropose.ts index 99a951c9..15dd4b1f 100644 --- a/src/services/contracts/baseDAO/hooks/useLambdaExecutePropose.ts +++ b/src/services/contracts/baseDAO/hooks/useLambdaExecutePropose.ts @@ -18,6 +18,7 @@ export const useLambdaExecutePropose = () => { { dao: LambdaDAO; args: LambdaExecuteArgs; handleClose: () => void } >( async ({ dao, args, handleClose }) => { + // debugger const { key: proposalNotification, closeSnackbar: closeProposalNotification } = openNotification({ message: "Proposal is being created...", persist: true, @@ -34,6 +35,7 @@ export const useLambdaExecutePropose = () => { tezosToolkit = connectedToolkit } + // debugger const data = await dao.proposeLambdaExecute(args, tezosToolkit) mixpanel.track("Proposal Created", { diff --git a/src/services/contracts/baseDAO/hooks/useTransfers.ts b/src/services/contracts/baseDAO/hooks/useTransfers.ts index 8bfc54b8..5e5fe81a 100644 --- a/src/services/contracts/baseDAO/hooks/useTransfers.ts +++ b/src/services/contracts/baseDAO/hooks/useTransfers.ts @@ -25,6 +25,7 @@ export interface TransferWithBN { hash: string type?: "Withdrawal" | "Deposit" token?: BNToken + status?: string } export const useTransfers = (contractAddress: string) => { diff --git a/src/services/contracts/baseDAO/lambdaDAO/index.ts b/src/services/contracts/baseDAO/lambdaDAO/index.ts index 5460a55a..929d6f01 100644 --- a/src/services/contracts/baseDAO/lambdaDAO/index.ts +++ b/src/services/contracts/baseDAO/lambdaDAO/index.ts @@ -200,6 +200,13 @@ export class LambdaDAO extends BaseDAO { return await contractMethod.send() } + /** + * This function sets of lambda in proposal to be executed + * @param param0 + * @param tezos + * @returns any + * + */ public async proposeLambdaExecute( { handler_name, agoraPostId, handler_code, handler_params, lambda_arguments }: LambdaExecuteArgs, tezos: TezosToolkit @@ -269,6 +276,7 @@ export class LambdaDAO extends BaseDAO { const contractMethod = contract.methods.propose( await tezos.wallet.pkh(), + // frozen_extra_value is 0 for lambda remove formatUnits(new BigNumber(this.data.extra.frozen_extra_value), this.data.token.decimals), proposalMetadata ) diff --git a/src/services/contracts/baseDAO/lambdaDAO/michelson/execlambda.ts b/src/services/contracts/baseDAO/lambdaDAO/michelson/execlambda.ts new file mode 100644 index 00000000..0ff2d095 --- /dev/null +++ b/src/services/contracts/baseDAO/lambdaDAO/michelson/execlambda.ts @@ -0,0 +1,4 @@ +export default `pair (lambda + (pair + (pair + (map %handler_storage string bytes) (bytes %packed_argument)) (pair %proposal_info (address %from) (nat %frozen_token) (bytes %proposal_metadata))) (pair (pair (option %guardian address) (map %handler_storage string bytes)) (list %operations operation))) bytes` diff --git a/src/services/lite/utils.ts b/src/services/lite/utils.ts index a7735917..e6e2f7cd 100644 --- a/src/services/lite/utils.ts +++ b/src/services/lite/utils.ts @@ -141,6 +141,35 @@ const getUsers = (options: Choice[]) => { return new Set(addresses) } +export const getGroupedVotes = (options: Choice[]) => { + const usersList = getUsers(options) + const array = Array.from(usersList) + + const groupedVotes = array.map((address: string) => { + const optionsList: any[] = [] + options.map((option: Choice) => { + option.walletAddresses.map(addressVote => { + if (addressVote.address === address) { + const obj = { + name: option.name, + balance: addressVote.balanceAtReferenceBlock, + cidLink: addressVote.cidLink, + signature: addressVote.signature, + payloadBytes: addressVote.payloadBytes + } + optionsList.push(obj) + } + }) + }) + const voteObj = { + address: address, + options: optionsList + } + return voteObj + }) + return groupedVotes +} + export const getTotalVoters = (choices: Choice[]) => { const totalVoters = getUsers(choices) diff --git a/src/theme/index.ts b/src/theme/index.ts index fa4c0a9c..54f56b2a 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -213,7 +213,8 @@ export const theme = createTheme({ borderBottom: "none" }, "&:before": { - borderBottom: "none" + borderBottom: "none", + transition: "none" }, "&:hover:not($disabled):not($focused):not($error):before": { borderBottom: "none" @@ -261,6 +262,9 @@ export const theme = createTheme({ color: "#bfc5ca !important", background: "inherit !important" } + }, + containedSecondary: { + backgroundColor: "#4ed092" } }, MuiInputBase: { diff --git a/tsconfig.json b/tsconfig.json index 5cabcdf5..05831fb4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { - "target": "es2017", + "target": "es2019", "baseUrl": "./src", "lib": [ "dom", "dom.iterable", "esnext" - ], + ], "types" : ["node", "lodash", "react"], "allowJs": true, "skipLibCheck": true, @@ -20,7 +20,10 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "paths": { + "react": [ "./node_modules/@types/react" ] + } }, "include": [ "src/global.d.ts", diff --git a/yarn.lock b/yarn.lock index ff06d93c..f34ba048 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4214,12 +4214,12 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@^17.0.2": - version "17.0.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" - integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== +"@types/react-dom@^18.2.25": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== dependencies: - "@types/react" "^17" + "@types/react" "*" "@types/react-html-parser@^2.0.2": version "2.0.6" @@ -4269,7 +4269,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17", "@types/react@^17.0.3", "@types/react@~17.0.3": +"@types/react@*", "@types/react@~17.0.3": version "17.0.80" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.80.tgz#a5dfc351d6a41257eb592d73d3a85d3b7dbcbb41" integrity sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA== @@ -4278,6 +4278,15 @@ "@types/scheduler" "^0.16" csstype "^3.0.2" +"@types/react@^17.0.44": + version "17.0.83" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.83.tgz#b477c56387b74279281149dcf5ba2a1e2216d131" + integrity sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "^0.16" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -5462,6 +5471,11 @@ asn1.js@^4.10.1: inherits "^2.0.1" minimalistic-assert "^1.0.0" +assert-never@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.3.0.tgz#c53cf3ad8fcdb67f400a941dea66dac7fe82dd2e" + integrity sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ== + assert@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd"