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 index 8cc4c015..8b137891 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d038b79c..2225fc21 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/valid-url": "^1.0.4", "@web3modal/wagmi": "^5.0.6", "assert": "^2.0.0", + "assert-never": "^1.2.1", "bignumber.js": "^9.0.1", "blockies-ts": "^1.0.0", "caniuse-lite": "", 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/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/NetworkSheet.tsx b/src/modules/explorer/components/NetworkSheet.tsx index ef4584dc..433d9c18 100644 --- a/src/modules/explorer/components/NetworkSheet.tsx +++ b/src/modules/explorer/components/NetworkSheet.tsx @@ -6,6 +6,7 @@ import { ResponsiveDialog } from "./ResponsiveDialog" import { ColorDot } from "./ChangeNetworkButton" import { ContentContainer } from "./ContentContainer" import { ActionTypes, CreatorContext } from "modules/creator/state" +import { ContentContainer } from "components/ui/Table" const SheetItem = styled(ContentContainer)({ "height": 50, diff --git a/src/modules/explorer/components/ProposalActionsDialog.tsx b/src/modules/explorer/components/ProposalActionsDialog.tsx index 4545c046..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 @@ -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/ProposalsList.tsx b/src/modules/explorer/components/ProposalsList.tsx index c9049ff9..7fd4e3f9 100644 --- a/src/modules/explorer/components/ProposalsList.tsx +++ b/src/modules/explorer/components/ProposalsList.tsx @@ -1,15 +1,12 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useCallback, useEffect, useMemo, useState } from "react" -import { CircularProgress, Collapse, Grid, Typography, styled } from "@material-ui/core" +import { Collapse, Grid, styled, Typography } from "@material-ui/core" 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 { ProposalTableRow } from "modules/lite/explorer/components/ProposalTableRow" import { Poll } from "models/Polls" import ReactPaginate from "react-paginate" import "../pages/DAOList/styles.css" -import { Filters } from "../pages/User/components/UserMovements" -import { Order, ProposalType, StatusOption } from "./FiltersUserDialog" const TableContainer = styled(Grid)({ width: "100%" @@ -26,11 +23,6 @@ const ProposalsFooter = styled(Grid)({ minHeight: 34 }) -const LoaderContainer = styled(Grid)({ - paddingTop: 40, - paddingBottom: 40 -}) - interface Props { currentLevel: number proposals: Proposal[] | undefined @@ -38,22 +30,15 @@ interface Props { rightItem?: (proposal: Proposal) => React.ReactElement liteProposals: Poll[] | undefined proposalStyle?: any - showFullList?: boolean - filters: undefined | Filters -} - -interface ProposalObj { - type: string - proposal: Proposal | Poll } export const ProposalsList: React.FC = ({ currentLevel, proposals, + showFooter, + rightItem, liteProposals, - proposalStyle, - showFullList = true, - filters = undefined + proposalStyle }) => { const [currentPage, setCurrentPage] = useState(0) const [offset, setOffset] = useState(0) @@ -63,41 +48,22 @@ export const ProposalsList: React.FC = ({ const [filterOnchain, setFilterOnchain] = useState() const [isLoading, setIsLoading] = useState(false) - const listOfProposals = useMemo(() => { - const proposalList: { type: string; proposal: Proposal | Poll }[] = [] - proposals?.map(proposal => { - const item = { - type: "lambda", - proposal: proposal - } - proposalList.push(item) - return - }) - liteProposals?.map(proposal => { - const item = { - type: "lite", - proposal: proposal - } - proposalList.push(item) - return - }) - return proposalList - }, [liteProposals, proposals]) - - useEffect(() => { - setFilteredProposals(listOfProposals) - }, []) - - useEffect(() => { - setFilteredProposals(listOfProposals) - }, [showFullList]) - - const pageCount = Math.ceil(filteredProposals ? filteredProposals.length / 4 : 0) + const itemsPerPage = 24 + const pageCount = Math.ceil( + proposals && liteProposals + ? proposals.length + liteProposals.length / itemsPerPage + : proposals && liteProposals?.length === undefined + ? proposals.length / itemsPerPage + : proposals?.length === undefined && liteProposals + ? liteProposals.length / itemsPerPage + : 0 + ) // Invoke when user click to request another page. const handlePageClick = (event: { selected: number }) => { - if (filteredProposals) { - setOffset((event.selected * 4) % filteredProposals.length) + if (proposals) { + const newOffset = (event.selected * itemsPerPage) % proposals.length + setOffset(newOffset) setCurrentPage(event.selected) } } @@ -208,54 +174,66 @@ export const ProposalsList: React.FC = ({ return ( - {isLoading ? ( - - - - ) : ( - <> - - {filteredProposals && filteredProposals.length && filteredProposals.length > 0 ? ( - - {filteredProposals.slice(offset, offset + 4).map((p, i) => - p.type === "lambda" ? ( - - - - - - ) : ( -
- -
- ) - )} -
- ) : ( - No proposals found - )} + + {proposals && proposals.length && proposals.length > 0 ? ( + + {proposals.slice(offset, offset + itemsPerPage).map((p, i) => ( + + + + {rightItem ? rightItem(p) : null} + + + + ))} - {showFullList ? ( - - + ) : null} + {liteProposals && liteProposals.length > 0 + ? liteProposals.slice(offset, offset + itemsPerPage).map((poll, i) => { + return ( +
+ +
+ ) + }) + : null} + + {showFooter && ( + + + + + View All Proposals + + - ) : null} - - )} + + )} +
+ + +
) } diff --git a/src/modules/explorer/components/ResponsiveDialog.tsx b/src/modules/explorer/components/ResponsiveDialog.tsx index 7527a265..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")]: { @@ -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/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/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 f007a288..86cb31f0 100644 --- a/src/modules/explorer/pages/DAOList/index.tsx +++ b/src/modules/explorer/pages/DAOList/index.tsx @@ -117,6 +117,65 @@ export const DAOList: React.FC = () => { const { network, account, etherlink } = useTezos() const { data: daos, isLoading } = useAllDAOs(network) + const theme = useTheme() + const isMobileSmall = useMediaQuery(theme.breakpoints.down("mobile")) + const myDAOs = useMemo(() => { + if (daos) { + const formattedDAOs = daos + .map(dao => ({ + id: dao.address, + name: dao.name, + symbol: dao.token.symbol, + votingAddresses: dao.ledgers ? dao.ledgers.map(l => l.holder.address) : [], + votingAddressesCount: + dao.dao_type.name === "lite" ? dao.votingAddressesCount : dao.ledgers ? dao.ledgers?.length : 0, + dao_type: { + name: dao.dao_type.name + }, + description: dao.description, + allowPublicAccess: dao.dao_type.name === "lite" ? dao.allowPublicAccess : true + })) + .sort((a, b) => b.votingAddresses.length - a.votingAddresses.length) + + if (searchText) { + return formattedDAOs.filter( + formattedDao => + (formattedDao.name && formattedDao.name.toLowerCase().includes(searchText.toLowerCase())) || + (formattedDao.symbol && formattedDao.symbol.toLowerCase().includes(searchText.toLowerCase())) + ) + } + return formattedDAOs.filter(dao => dao.votingAddresses.includes(account)) + } + + return [] + }, [daos, account, searchText]) + + if (!account) return + + if (isLoading) + return ( + + + + ) + + if (myDAOs.length === 0) return You have not joined any DAO + + return ( + + {myDAOs.map((dao, i) => ( + + + + ))} + + ) +} + +export const DAOList: React.FC = () => { + const { network } = useTezos() + const { data: daos, isLoading } = useAllDAOs(network) + const theme = useTheme() const isMobileExtraSmall = useMediaQuery(theme.breakpoints.down("xs")) const isMobileSmall = useMediaQuery(theme.breakpoints.down("mobile")) diff --git a/src/modules/explorer/pages/Proposals/index.tsx b/src/modules/explorer/pages/Proposals/index.tsx index e88818da..1de9f47c 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 ( <> @@ -273,7 +280,6 @@ export const Proposals: React.FC = () => { currentLevel={cycleInfo.currentLevel} proposals={proposals} liteProposals={undefined} - filters={filters} /> )} {!(proposals && proposals.length > 0) ? ( @@ -296,7 +302,6 @@ export const Proposals: React.FC = () => { currentLevel={cycleInfo.currentLevel} proposals={undefined} liteProposals={polls} - filters={filters} /> )} {!(polls && polls.length > 0) ? ( @@ -312,7 +317,9 @@ export const Proposals: React.FC = () => { - + + + {/* Keeping this component here as it is inhe master branch */} )} {!(proposalsCreated && proposalsCreated.length > 0) && !(pollsPosted && pollsPosted.length > 0) ? ( @@ -329,9 +327,7 @@ export const UserMovements: React.FC<{ )} {!(proposalsVoted && proposalsVoted.length > 0) && !(votedPolls && votedPolls.length > 0) ? ( 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/services/aci/endpoint.ts b/src/services/aci/endpoint.ts new file mode 100644 index 00000000..ec4875b4 --- /dev/null +++ b/src/services/aci/endpoint.ts @@ -0,0 +1,361 @@ +import { TezosToolkit } from "@taquito/taquito" +import { validateAddress, encodePubKey, encodeKey, encodeKeyHash } from "@taquito/utils" +import { TokenSchema, Schema } from "@taquito/michelson-encoder" +import { rpcNodes } from "services/beacon" +import { assertNever } from "assert-never" +import type { token, tokenValueType } from "." +import type { MichelineMichelsonV1Expression } from "@airgap/beacon-sdk" + +function getFieldName(id: any): string { + return `input-${id.toString()}` +} + +function initTokenTable(init: any, counter: any, defaultInit: tokenValueType = "") { + init[getFieldName(counter)] = defaultInit +} + +function parseSchema(counter: number, token: TokenSchema, 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) { + 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 "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 + 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..c31a80f6 --- /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, + 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) + } +} +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..a607c742 --- /dev/null +++ b/src/services/aci/useArbitratyContractData.ts @@ -0,0 +1,67 @@ +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" + +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() + } + + 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/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`