formik#FormikContext TypeScript Examples

The following examples show how to use formik#FormikContext. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Wallet.tsx    From panvala with Apache License 2.0 4 votes vote down vote up
Wallet: React.SFC = () => {
  const {
    account,
    contracts: { gatekeeper },
    onRefreshBalances,
  }: IEthereumContext = React.useContext(EthereumContext);

  // // list of enabled accounts. use to populate wallet list
  // const enabledAccounts = loadState(ENABLED_ACCOUNTS);
  // delegate (metamask) hot wallet
  const [hotWallet, setHotWallet] = React.useState('');
  // voter (trezor/ledger) cold wallet
  const [coldWallet, setColdWallet] = React.useState('');

  const [step, setStep] = React.useState(0);
  const [confirmed, setConfirmed] = React.useState({
    coldWallet: false,
    hotWallet: false,
  });
  const [txPending, setTxPending] = React.useState(false);

  function setLinkedWallet(type: string, address: string) {
    setConfirmed({
      ...confirmed,
      [type]: false,
    });
    if (type === 'hotWallet') {
      setHotWallet(address);
    } else if (type === 'coldWallet') {
      setColdWallet(address);
    }
  }
  function confirmColdWallet() {
    const linkedWallets = loadState(LINKED_WALLETS);
    saveState(LINKED_WALLETS, {
      ...linkedWallets,
      coldWallet,
    });
    setConfirmed({
      ...confirmed,
      coldWallet: true,
    });
  }
  function confirmHotWallet() {
    const linkedWallets = loadState(LINKED_WALLETS);
    saveState(LINKED_WALLETS, {
      ...linkedWallets,
      hotWallet,
    });
    setConfirmed({
      ...confirmed,
      hotWallet: true,
    });
  }

  React.useEffect(() => {
    // get persisted state from local storage
    const linkedWallets = loadState(LINKED_WALLETS);

    if (account) {
      if (!linkedWallets) {
        // no link, fill in hot
        setHotWallet(account);
      } else {
        if (linkedWallets.hotWallet === account) {
          // signed in as hot
          setHotWallet(account);
        } else if (linkedWallets.hotWallet) {
          // signed in, but not as hot
          setHotWallet(linkedWallets.hotWallet);

          if (confirmed.hotWallet) {
            // confirmed hot, time to set cold
            if (linkedWallets.coldWallet) {
              // cold exists
              setColdWallet(linkedWallets.coldWallet);
            } else {
              // no cold set, fill in cold
              setColdWallet(account);
            }
          }
        }
      }
    }
  }, [account, confirmed.hotWallet]);

  async function linkDelegateVoter() {
    const linkedWallets = loadState(LINKED_WALLETS);
    if (
      !linkedWallets ||
      linkedWallets.hotWallet !== hotWallet ||
      linkedWallets.coldWallet !== coldWallet
    ) {
      console.error('Linked wallets not in sync with local storage');
      return;
    }
    // TODO: (might not be possible) check if hotWallet is an unlocked account in metamask
    setStep(1);

    if (!isEmpty(gatekeeper)) {
      try {
        if (typeof gatekeeper.functions.delegateVotingRights !== 'undefined') {
          setTxPending(true);
          const response = await gatekeeper.functions.delegateVotingRights(hotWallet);

          await response.wait();
          setTxPending(false);
          setStep(2);
          toast.success('delegate voter tx mined');
        } else {
          toast.info('This feature is not supported yet');
        }

        // save to local storage
        saveState(LINKED_WALLETS, {
          hotWallet,
          coldWallet,
        });

        onRefreshBalances();
      } catch (error) {
        handleLinkError(error);
      }
    } else {
      throw new Error(ETHEREUM_NOT_AVAILABLE);
    }
  }

  function handleLinkError(error: Error) {
    // Return the user to where they were
    setTxPending(false);
    setStep(0);

    // Display message
    const errorType = handleGenericError(error, toast);
    if (errorType) {
      toast.error(`Problem linking wallets: ${error.message}`);
    }
    console.error(error);
  }

  const steps = [
    <>
      <Text textAlign="center" fontSize={'1.5rem'} m={0}>
        Link hot and cold wallets
      </Text>
      <RouterLink href="/slates" as="/slates">
        <CancelButton>Cancel</CancelButton>
      </RouterLink>

      {/* prettier-ignore */}
      <Text lineHeight={1.5}>
        Please sign in with and confirm your <A bold color="blue">hot wallet</A>.
        Your hot wallet will be your delegated voting wallet.
        Then sign in with the <A bold color="blue">cold wallet</A> (Ledger or Trezor)
        you would like to link the hot wallet with.
      </Text>

      <Formik
        enableReinitialize={true}
        initialValues={{ hotWallet, coldWallet: confirmed.hotWallet ? coldWallet : '' }}
        onSubmit={async (_: IFormValues, { setSubmitting, setFieldError }) => {
          if (coldWallet.toLowerCase() !== account.toLowerCase()) {
            setFieldError('coldWallet', 'Please connect your cold wallet');
            return;
          }

          await linkDelegateVoter();

          // re-enable submit button
          setSubmitting(false);
        }}
      >
        {({
          values,
          isSubmitting,
          handleChange,
          handleSubmit,
          setFieldError,
          setFieldValue,
          setFieldTouched,
        }: FormikContext<IFormValues>) => {
          return (
            <Form>
              <Label htmlFor="hotWallet">{'Select hot wallet'}</Label>
              <Flex justifyStart noWrap alignCenter>
                <Identicon address={hotWallet} diameter={20} />
                <Input
                  m={2}
                  fontFamily="Fira Code"
                  name="hotWallet"
                  onChange={(e: any) => {
                    handleChange(e);
                    setFieldTouched('hotWallet');

                    // clear confirmations
                    setConfirmed({
                      hotWallet: false,
                      coldWallet: false,
                    });

                    // clear cold wallet
                    setFieldValue('coldWallet', '');
                  }}
                  value={values.hotWallet}
                />
                <Button
                  width="100px"
                  type="default"
                  onClick={(e: any) => {
                    e.preventDefault(); // Do not submit form

                    // validate
                    if (!isAddress(values.hotWallet.toLowerCase())) {
                      setFieldError('hotWallet', 'Invalid address');
                      return;
                    }

                    confirmHotWallet();
                  }}
                  bg={confirmed.hotWallet ? 'greens.light' : ''}
                  disabled={!values.hotWallet}
                >
                  {confirmed.hotWallet ? 'Confirmed' : 'Confirm'}
                </Button>
              </Flex>
              <ErrorMessage name="hotWallet" component="span" />
              <Text mt={0} mb={4} fontSize={0} color="grey">
                Reminder: This is the address that will be able to vote with your PAN.
              </Text>

              <Label htmlFor="coldWallet">{'Select cold wallet'}</Label>
              <Flex justifyStart noWrap alignCenter>
                <Identicon address={coldWallet} diameter={20} />
                <Input
                  m={2}
                  fontFamily="Fira Code"
                  name="coldWallet"
                  onChange={(e: any) => {
                    handleChange(e);
                    setFieldTouched('coldWallet');
                    setConfirmed({ ...confirmed, coldWallet: false });
                  }}
                  value={values.coldWallet}
                  disabled={!confirmed.hotWallet}
                />
                <Button
                  width="100px"
                  type="default"
                  onClick={(e: any) => {
                    e.preventDefault(); // Do not submit form

                    // validate -- must be valid address different from hot wallet, and user must
                    // be logged in with this account
                    if (!isAddress(values.coldWallet.toLowerCase())) {
                      setFieldError('coldWallet', 'Invalid address');
                      return;
                    }

                    if (values.hotWallet.toLowerCase() === values.coldWallet.toLowerCase()) {
                      setFieldError('coldWallet', 'Cold wallet must be different from hot wallet');
                      return;
                    }

                    confirmColdWallet();
                  }}
                  bg={confirmed.coldWallet ? 'greens.light' : ''}
                  disabled={!confirmed.hotWallet || !values.coldWallet}
                >
                  {confirmed.coldWallet ? 'Confirmed' : 'Confirm'}
                </Button>
              </Flex>
              <ErrorMessage name="coldWallet" component="span" />
              {/* prettier-ignore */}
              <Text mt={0} mb={4} fontSize={0} color="grey">
                This wallet must be connected.
                How to connect <A bold color="blue">Trezor</A> and <A bold color="blue">Ledger</A>.
              </Text>

              <Flex justifyEnd>
                <Button
                  width="200px"
                  large
                  type="default"
                  onClick={handleSubmit}
                  disabled={!confirmed.coldWallet || !confirmed.hotWallet || isSubmitting}
                >
                  Continue
                </Button>
              </Flex>
            </Form>
          );
        }}
      </Formik>
    </>,
    <div>
      <Text textAlign="center" fontSize={'1.5rem'} m={0}>
        Grant Permissions
      </Text>
      <CancelButton onClick={() => setStep(0)}>Cancel</CancelButton>

      <Text lineHeight={1.5}>
        By granting permissions in this transaction, you are allowing the contract to lock your PAN.
        You are not relinquishing control of your PAN and can withdraw it at anytime. Linking your
        hot and cold wallet will enable you to vote while your PAN is still stored in your cold
        wallet.
      </Text>

      <StepperMetamaskDialog />
    </div>,
    <>
      <Text textAlign="center" fontSize={'1.5rem'} mt={2} mb={4}>
        Wallets linked!
      </Text>

      <Text lineHeight={1.5} px={3}>
        You have now linked your cold and hot wallets. You can change these settings any time on the
        ballot screen or when you go to vote.
      </Text>

      <Box p={3}>
        <Text fontWeight="bold" fontSize={0}>
          Linked Cold Wallet
        </Text>
        <Flex justifyBetween alignCenter>
          <Identicon address={coldWallet} diameter={20} />
          <Box fontSize={1} fontFamily="fira code" px={2}>
            {splitAddressHumanReadable(coldWallet).slice(0, 30)} ...
          </Box>
          <Tag color="blue" bg="blues.light">
            COLD WALLET
          </Tag>
        </Flex>
      </Box>

      <Box p={3}>
        <Text fontWeight="bold" fontSize={0}>
          Active Hot Wallet
        </Text>
        <Flex justifyBetween alignCenter>
          <Identicon address={hotWallet} diameter={20} />
          <Box fontSize={1} fontFamily="fira code" px={2}>
            {splitAddressHumanReadable(hotWallet).slice(0, 30)} ...
          </Box>
          <Tag color="red" bg="reds.light">
            HOT WALLET
          </Tag>
        </Flex>
      </Box>

      <Flex justifyEnd>
        <Button
          width="150px"
          large
          type="default"
          onClick={null}
          disabled={
            (hotWallet &&
              coldWallet &&
              utils.getAddress(hotWallet) === utils.getAddress(coldWallet)) ||
            false
          }
        >
          Continue
        </Button>
      </Flex>
    </>,
  ];

  return (
    <>
      <PendingTransaction isOpen={txPending} setOpen={setTxPending} />
      <Flex justifyCenter>
        <Box
          display="flex"
          flexDirection="column"
          alignItems="center"
          width={550}
          p={4}
          mt={[2, 3, 5, 6]}
          borderRadius={10}
          boxShadow={0}
        >
          <Box position="relative">{steps[step]}</Box>
        </Box>
      </Flex>
    </>
  );
}
Example #2
Source File: governance.tsx    From panvala with Apache License 2.0 4 votes vote down vote up
CreateGovernanceSlate: StatelessPage<any> = () => {
  // modal opener
  const [isOpen, setOpenModal] = React.useState(false);
  const { onRefreshSlates, onRefreshCurrentBallot, currentBallot }: IMainContext = React.useContext(
    MainContext
  );
  // get eth context
  const {
    account,
    contracts,
    onRefreshBalances,
    slateStakeAmount,
    gkAllowance,
  }: IEthereumContext = React.useContext(EthereumContext);
  const [pendingText, setPendingText] = React.useState('');
  const [pageStatus, setPageStatus] = React.useState(PageStatus.Loading);
  const [deadline, setDeadline] = React.useState(0);

  // parameters
  const initialParameters = {
    slateStakeAmount: {
      oldValue: slateStakeAmount.toString(),
      newValue: '',
      type: 'uint256',
      key: 'slateStakeAmount',
    },
    gatekeeperAddress: {
      oldValue: contracts.gatekeeper.address,
      newValue: '',
      type: 'address',
      key: 'gatekeeperAddress',
    },
  };

  // Update page status when ballot info changes
  React.useEffect(() => {
    const newDeadline = currentBallot.slateSubmissionDeadline.GOVERNANCE;
    setDeadline(newDeadline);

    if (pageStatus === PageStatus.Loading) {
      if (newDeadline === 0) return;

      setPageStatus(PageStatus.Initialized);
    } else {
      if (!isSlateSubmittable(currentBallot, 'GOVERNANCE')) {
        setPageStatus(PageStatus.SubmissionClosed);
        // if (typeof router !== 'undefined') {
        //   router.push('/slates');
        // }
      } else {
        setPageStatus(PageStatus.SubmissionOpen);
      }
    }
  }, [currentBallot.slateSubmissionDeadline, pageStatus]);

  // pending tx loader
  const [txsPending, setTxsPending] = React.useState(0);

  function calculateNumTxs(values) {
    let numTxs: number = 1; // gk.recommendSlate

    if (values.recommendation === 'governance') {
      numTxs += 1; // ps.createManyProposals
    }

    if (values.stake === 'yes') {
      numTxs += 1; // gk.stakeSlate
      if (gkAllowance.lt(slateStakeAmount)) {
        numTxs += 1; // token.approve
      }
    }

    return numTxs;
  }

  // Condense to the bare parameter changes
  function filterParameterChanges(
    formParameters: IParameterChangesObject
  ): IParameterChangesObject {
    return Object.keys(formParameters).reduce((acc, paramKey) => {
      let value = clone(formParameters[paramKey]);
      if (!!value.newValue) {
        // Convert values if necessary first before checking equality
        if (paramKey === 'slateStakeAmount') {
          // Convert token amount
          value.newValue = convertedToBaseUnits(value.newValue, 18);
        } else if (value.type === 'address') {
          // Normalize address
          value.oldValue = getAddress(value.oldValue);
          value.newValue = getAddress(value.newValue.toLowerCase());
        }

        // if something has changed, add it
        if (value.newValue !== value.oldValue) {
          return {
            ...acc,
            [paramKey]: value,
          };
        }
      }

      return acc;
    }, {});
  }

  // Submit slate information to the Gatekeeper, saving metadata in IPFS
  async function handleSubmitSlate(
    values: IGovernanceSlateFormValues,
    parameterChanges: IParameterChangesObject
  ) {
    console.log('parameterChanges:', parameterChanges);

    let errorMessage = '';

    try {
      if (!account) {
        throw new Error(ETHEREUM_NOT_AVAILABLE);
      }

      const numTxs = calculateNumTxs(values);
      setTxsPending(numTxs);
      setPendingText('Adding proposals to IPFS...');

      const paramKeys = Object.keys(parameterChanges);

      const proposalMetadatas: IGovernanceProposalMetadata[] = paramKeys.map(
        (param: string): IGovernanceProposalMetadata => {
          const { oldValue, newValue, type, key } = parameterChanges[param];

          return {
            firstName: values.firstName,
            lastName: values.lastName,
            summary: values.summary,
            organization: values.organization,
            parameterChanges: {
              key,
              oldValue,
              newValue,
              type,
            },
          };
        }
      );

      const proposalMultihashes: Buffer[] = await Promise.all(
        proposalMetadatas.map(async (metadata: IGovernanceProposalMetadata) => {
          try {
            const multihash: string = await saveToIpfs(metadata);
            // we need a buffer of the multihash for the transaction
            return Buffer.from(multihash);
          } catch (error) {
            return error;
          }
        })
      );
      const proposalInfo: IGovernanceProposalInfo = {
        metadatas: proposalMetadatas,
        multihashes: proposalMultihashes,
      };

      // save proposal metadata to IPFS to be included in the slate metadata
      console.log('preparing proposals...');

      setPendingText('Including proposals in slate (check MetaMask)...');
      // 1. create proposal and get request ID
      const emptySlate = values.recommendation === 'noAction';
      const getRequests = emptySlate
        ? Promise.resolve([])
        : sendCreateManyGovernanceProposals(contracts.parameterStore, proposalInfo);

      errorMessage = 'error adding proposal metadata.';
      // console.log('creating proposals...');
      const requestIDs = await getRequests;

      setPendingText('Adding slate to IPFS...');
      const resource = contracts.parameterStore.address;

      const slateMetadata: IGovernanceSlateMetadataV2 = {
        firstName: values.firstName,
        lastName: values.lastName,
        description: values.summary,
        organization: values.organization,
        proposalMultihashes: proposalMultihashes.map(md => md.toString()),
        proposals: proposalMetadatas,
      };

      console.log('slateMetadata:', slateMetadata);

      errorMessage = 'error saving slate metadata.';
      console.log('saving slate metadata...');
      const slateMetadataHash: string = await saveToIpfs(slateMetadata);

      setPendingText('Creating governance slate (check MetaMask)...');
      // Submit the slate info to the contract
      errorMessage = 'error submitting slate.';
      const slate: any = await sendRecommendGovernanceSlateTx(
        contracts.gatekeeper,
        resource,
        requestIDs,
        slateMetadataHash
      );
      console.log('Submitted slate', slate);

      setPendingText('Saving slate...');
      // Add slate to db
      const slateToSave: ISaveSlate = {
        slateID: slate.slateID,
        metadataHash: slateMetadataHash,
        email: values.email,
        proposalInfo,
      };

      errorMessage = 'problem saving slate info.';
      const response = await postSlate(slateToSave);
      if (response.status === 200) {
        console.log('Saved slate info');
        toast.success('Saved slate');
        if (values.stake === 'yes') {
          if (gkAllowance.lt(slateStakeAmount)) {
            setPendingText('Approving the Gatekeeper to stake on slate (check MetaMask)...');
            await contracts.token.approve(contracts.gatekeeper.address, MaxUint256);
          }
          setPendingText('Staking on slate (check MetaMask)...');
          const res = await contracts.gatekeeper.functions.stakeTokens(slate.slateID);
          await res.wait();
        }

        setTxsPending(0);
        setPendingText('');
        setOpenModal(true);
        onRefreshSlates();
        onRefreshCurrentBallot();
        onRefreshBalances();
      } else {
        throw new Error(`ERROR: failed to save slate: ${JSON.stringify(response)}`);
      }
    } catch (error) {
      errorMessage = `ERROR: ${errorMessage}: ${error.message}`;
      handleSubmissionError(errorMessage, error);
    }

    // TODO: Should take us to all slates view after successful submission
  }

  function handleSubmissionError(errorMessage, error) {
    // Reset view
    setOpenModal(false);
    setTxsPending(0);

    // Show a message
    const errorType = handleGenericError(error, toast);
    if (errorType) {
      toast.error(`Problem submitting slate: ${errorMessage}`);
    }

    console.error(error);
  }

  if (pageStatus === PageStatus.Loading || pageStatus === PageStatus.Initialized) {
    return <div>Loading...</div>;
  }

  return pageStatus === PageStatus.SubmissionOpen ? (
    <>
      <CenteredTitle title="Create a Governance Slate" />

      <CenteredWrapper>
        <Formik
          initialValues={
            process.env.NODE_ENV === 'development'
              ? {
                  email: '[email protected]',
                  firstName: 'First',
                  lastName: 'Last',
                  organization: 'Ethereum',
                  summary: 'fdsfdsfasdfadsfsad',
                  parameters: initialParameters,
                  recommendation: 'governance',
                  stake: 'no',
                }
              : {
                  email: '',
                  firstName: '',
                  lastName: '',
                  organization: '',
                  summary: '',
                  parameters: initialParameters,
                  recommendation: 'governance',
                  stake: 'no',
                }
          }
          validationSchema={GovernanceSlateFormSchema}
          onSubmit={async (
            values: IGovernanceSlateFormValues,
            { setSubmitting, setFieldError }: any
          ) => {
            const emptySlate = values.recommendation === 'noAction';

            if (emptySlate) {
              // Submit with no changes if the user selected noAction
              const noChanges: IParameterChangesObject = {};
              await handleSubmitSlate(values, noChanges);
            } else {
              try {
                const changes: IParameterChangesObject = filterParameterChanges(values.parameters);
                if (Object.keys(changes).length === 0) {
                  setFieldError(
                    'parametersForm',
                    'You must enter some parameter values different from the old ones'
                  );
                } else {
                  await handleSubmitSlate(values, changes);
                }
              } catch (error) {
                // some issue with filtering the changes - should never get here
                const errorType = handleGenericError(error, toast);
                if (errorType) {
                  toast.error(`Problem submitting slate: ${error.message}`);
                }
                console.error(error);
              }
            }

            // re-enable submit button
            setSubmitting(false);
          }}
        >
          {({
            isSubmitting,
            values,
            setFieldValue,
            handleSubmit,
            errors,
          }: FormikContext<IGovernanceSlateFormValues>) => (
            <Box>
              <Form>
                <Box p={4}>
                  <SectionLabel>{'ABOUT'}</SectionLabel>

                  <FieldText required label={'Email'} name="email" placeholder="Enter your email" />

                  <FieldText
                    required
                    label={'First Name'}
                    name="firstName"
                    placeholder="Enter your first name"
                  />
                  <FieldText
                    label={'Last Name'}
                    name="lastName"
                    placeholder="Enter your last name"
                  />
                  <FieldText
                    label={'Organization Name'}
                    name="organization"
                    placeholder="Enter your organization's name"
                  />

                  <FieldTextarea
                    required
                    label={'Description'}
                    name="summary"
                    placeholder="Enter a summary for your slate"
                  />
                </Box>

                <Separator />
                <Box p={4}>
                  <SectionLabel>{'RECOMMENDATION'}</SectionLabel>
                  <Label htmlFor="recommendation" required>
                    {'What type of recommendation would you like to make?'}
                  </Label>
                  <ErrorMessage name="recommendation" component="span" />
                  <div>
                    <Checkbox
                      name="recommendation"
                      value="governance"
                      label="Recommend governance proposals"
                    />
                    <Checkbox name="recommendation" value="noAction" label="Recommend no action" />
                  </div>
                  <div>
                    By recommending no action you are opposing any current or future slates for this
                    batch.
                  </div>
                </Box>

                <Separator />
                <Box p={4}>
                  {values.recommendation === 'governance' && (
                    <>
                      <SectionLabel>{'PARAMETERS'}</SectionLabel>
                      <ParametersForm
                        onChange={setFieldValue}
                        parameters={values.parameters}
                        errors={errors}
                      />
                    </>
                  )}

                  <Separator />
                  <Box p={4}>
                    <SectionLabel>STAKE</SectionLabel>
                    <Label htmlFor="stake" required>
                      {`Would you like to stake ${formatPanvalaUnits(
                        slateStakeAmount
                      )} tokens for this slate? This makes your slate eligible for the current batch.`}
                    </Label>
                    <ErrorMessage name="stake" component="span" />
                    <div>
                      <Checkbox name="stake" value="yes" label="Yes" />
                      <RadioSubText>
                        By selecting yes, you will stake tokens for your own slate and not have to
                        rely on others to stake tokens for you.
                      </RadioSubText>
                      <Checkbox name="stake" value="no" label="No" />
                      <RadioSubText>
                        By selecting no, you will have to wait for others to stake tokens for your
                        slate or you can stake tokens after you have created the slate.
                      </RadioSubText>
                    </div>
                  </Box>
                </Box>

                <Separator />
              </Form>

              <Flex p={4} justifyEnd>
                <BackButton />
                <Button type="submit" large primary disabled={isSubmitting} onClick={handleSubmit}>
                  {'Create Slate'}
                </Button>
              </Flex>
            </Box>
          )}
        </Formik>
      </CenteredWrapper>

      <Loader
        isOpen={txsPending > 0}
        setOpen={() => setTxsPending(0)}
        numTxs={txsPending}
        pendingText={pendingText}
      />

      <Modal handleClick={() => setOpenModal(false)} isOpen={isOpen}>
        <>
          <Image src="/static/check.svg" alt="slate submitted" width="80px" />
          <ModalTitle>{'Slate submitted.'}</ModalTitle>
          <ModalDescription>
            Now that your slate has been created you and others have the ability to stake tokens on
            it to propose it to token holders. Once there are tokens staked on the slate it will be
            eligible for a vote.
          </ModalDescription>
          <RouterLink href="/slates" as="/slates">
            <Button type="default">{'Done'}</Button>
          </RouterLink>
        </>
      </Modal>
    </>
  ) : (
    <ClosedSlateSubmission deadline={deadline} category={'governance'} />
  );
}
Example #3
Source File: grant.tsx    From panvala with Apache License 2.0 4 votes vote down vote up
CreateGrantSlate: StatelessPage<IProps> = ({ query }) => {
  // get proposals and eth context
  const {
    slates,
    slatesByID,
    proposals,
    currentBallot,
    onRefreshSlates,
    onRefreshCurrentBallot,
  }: IMainContext = React.useContext(MainContext);
  const {
    account,
    contracts,
    onRefreshBalances,
    slateStakeAmount,
    gkAllowance,
    votingRights,
    panBalance,
  }: IEthereumContext = React.useContext(EthereumContext);
  const [pendingText, setPendingText] = React.useState('');
  const [pageStatus, setPageStatus] = React.useState(PageStatus.Loading);
  const [deadline, setDeadline] = React.useState(0);

  // Update page status when ballot info changes
  React.useEffect(() => {
    const newDeadline = currentBallot.slateSubmissionDeadline.GRANT;
    setDeadline(newDeadline);

    if (pageStatus === PageStatus.Loading) {
      if (newDeadline === 0) return;

      setPageStatus(PageStatus.Initialized);
    } else {
      if (!isSlateSubmittable(currentBallot, 'GRANT')) {
        setPageStatus(PageStatus.SubmissionClosed);
        // if (typeof router !== 'undefined') {
        //   router.push('/slates');
        // }
      } else {
        setPageStatus(PageStatus.SubmissionOpen);
      }
    }
  }, [currentBallot.slateSubmissionDeadline, pageStatus]);

  // modal opener
  const [isOpen, setOpenModal] = React.useState(false);
  // pending tx loader
  const [txsPending, setTxsPending] = React.useState(0);
  const [availableTokens, setAvailableTokens] = React.useState('0');

  React.useEffect(() => {
    async function getProjectedAvailableTokens() {
      let winningSlate: ISlate | undefined;
      const lastEpoch = currentBallot.epochNumber - 1;
      try {
        const winningSlateID = await contracts.gatekeeper.functions.getWinningSlate(
          lastEpoch,
          contracts.tokenCapacitor.address
        );
        winningSlate = slatesByID[winningSlateID.toString()];
      } catch {} // if the query reverts, epoch hasn't been finalized yet

      const tokens = await projectedAvailableTokens(
        contracts.tokenCapacitor,
        contracts.gatekeeper,
        currentBallot.epochNumber,
        winningSlate
      );
      setAvailableTokens(tokens.toString());
    }
    if (
      !isEmpty(contracts.tokenCapacitor) &&
      !isEmpty(contracts.gatekeeper) &&
      contracts.tokenCapacitor.functions.hasOwnProperty('projectedUnlockedBalance')
    ) {
      getProjectedAvailableTokens();
    }
  }, [contracts, currentBallot.epochNumber, slates]);

  //  Submit proposals to the token capacitor and get corresponding request IDs
  async function getRequestIDs(proposalInfo: IGrantProposalInfo, tokenCapacitor: TokenCapacitor) {
    const { metadatas, multihashes: proposalMultihashes } = proposalInfo;
    // submit to the capacitor, get requestIDs
    // token distribution details
    const beneficiaries: string[] = metadatas.map(p => p.awardAddress);
    const tokenAmounts: string[] = metadatas.map(p => convertedToBaseUnits(p.tokensRequested, 18));
    console.log('tokenAmounts:', tokenAmounts);

    try {
      const response: TransactionResponse = await sendCreateManyProposalsTransaction(
        tokenCapacitor,
        beneficiaries,
        tokenAmounts,
        proposalMultihashes
      );

      // wait for tx to get mined
      const receipt: TransactionReceipt = await response.wait();

      if ('events' in receipt) {
        // Get the ProposalCreated logs from the receipt
        // Extract the requestIDs
        const requestIDs = (receipt as any).events
          .filter(event => event.event === 'ProposalCreated')
          .map(e => e.args.requestID);
        return requestIDs;
      }
      throw new Error('receipt did not contain any events');
    } catch (error) {
      throw error;
    }
  }

  // Submit requestIDs and metadataHash to the Gatekeeper.
  async function submitGrantSlate(requestIDs: any[], metadataHash: string): Promise<any> {
    if (!isEmpty(contracts.gatekeeper)) {
      const estimate = await contracts.gatekeeper.estimate.recommendSlate(
        contracts.tokenCapacitor.address,
        requestIDs,
        Buffer.from(metadataHash)
      );
      const txOptions = {
        gasLimit: estimate.add('100000').toHexString(),
        gasPrice: utils.parseUnits('9.0', 'gwei'),
      };
      const response = await (contracts.gatekeeper as any).functions.recommendSlate(
        contracts.tokenCapacitor.address,
        requestIDs,
        Buffer.from(metadataHash),
        txOptions
      );

      const receipt: ContractReceipt = await response.wait();

      if (typeof receipt.events !== 'undefined') {
        // Get the SlateCreated logs from the receipt
        // Extract the slateID
        const slateID = receipt.events
          .filter(event => event.event === 'SlateCreated')
          .map(e => e.args.slateID.toString());
        const slate: any = { slateID, metadataHash };
        console.log('Created slate', slate);
        return slate;
      }
    }
  }

  function calculateNumTxs(values, selectedProposals) {
    let numTxs: number = 1; // gk.recommendSlate

    if (selectedProposals.length > 0) {
      numTxs += 1; // tc.createManyProposals
    }

    if (values.stake === 'yes') {
      numTxs += 1; // gk.stakeSlate
      if (gkAllowance.lt(slateStakeAmount)) {
        numTxs += 1; // token.approve
      }
    }

    return numTxs;
  }

  function handleSubmissionError(errorMessage, error) {
    // Reset view
    setOpenModal(false);
    setTxsPending(0);

    // Show a message
    const errorType = handleGenericError(error, toast);
    if (errorType) {
      toast.error(`Problem submitting slate: ${errorMessage}`);
    }

    console.error(error);
  }

  /**
   * Submit slate information to the Gatekeeper, saving metadata in IPFS
   *
   * add proposals to ipfs, get multihashes
   * send tx to token_capacitor: createManyProposals (with multihashes)
   * get requestIDs from events
   * add slate to IPFS with metadata
   * send tx to gate_keeper: recommendSlate (with requestIDs & slate multihash)
   * get slateID from event
   * add slate to db: slateID, multihash
   */
  async function handleSubmitSlate(values: IFormValues, selectedProposals: IProposal[]) {
    let errorMessagePrefix = '';
    try {
      if (!account || !onRefreshSlates || !contracts) {
        throw new Error(ETHEREUM_NOT_AVAILABLE);
      }

      const numTxs = calculateNumTxs(values, selectedProposals);
      setTxsPending(numTxs);
      setPendingText('Adding proposals to IPFS...');

      // save proposal metadata to IPFS to be included in the slate metadata
      console.log('preparing proposals...');
      const proposalMultihashes: Buffer[] = await Promise.all(
        selectedProposals.map(async (metadata: IGrantProposalMetadata) => {
          try {
            const multihash: string = await saveToIpfs(metadata);
            // we need a buffer of the multihash for the transaction
            return Buffer.from(multihash);
          } catch (error) {
            return error;
          }
        })
      );
      // TODO: add proposal multihashes to db

      setPendingText('Including proposals in slate (check MetaMask)...');
      // Only use the metadata from here forward - do not expose private information
      const proposalMetadatas: IGrantProposalMetadata[] = selectedProposals.map(proposal => {
        return {
          firstName: proposal.firstName,
          lastName: proposal.lastName,
          title: proposal.title,
          summary: proposal.summary,
          tokensRequested: proposal.tokensRequested,
          github: proposal.github,
          id: proposal.id,
          website: proposal.website,
          organization: proposal.organization,
          recommendation: proposal.recommendation,
          projectPlan: proposal.projectPlan,
          projectTimeline: proposal.projectTimeline,
          teamBackgrounds: proposal.teamBackgrounds,
          otherFunding: proposal.otherFunding,
          awardAddress: proposal.awardAddress,
        };
      });

      const emptySlate = proposalMetadatas.length === 0;

      // 1. batch create proposals and get request IDs
      const proposalInfo: IGrantProposalInfo = {
        metadatas: proposalMetadatas,
        multihashes: proposalMultihashes,
      };

      errorMessagePrefix = 'error preparing proposals';
      const getRequests = emptySlate
        ? Promise.resolve([])
        : await getRequestIDs(proposalInfo, contracts.tokenCapacitor);

      const requestIDs = await getRequests;

      setPendingText('Adding slate to IPFS...');

      const slateMetadata: ISlateMetadata = {
        firstName: values.firstName,
        lastName: values.lastName,
        organization: values.organization,
        description: values.description,
        proposalMultihashes: proposalMultihashes.map(md => md.toString()),
        proposals: proposalMetadatas,
      };
      // console.log(slateMetadata);

      console.log('saving slate metadata...');
      errorMessagePrefix = 'error saving slate metadata';
      const slateMetadataHash: string = await saveToIpfs(slateMetadata);

      // Submit the slate info to the contract
      errorMessagePrefix = 'error submitting slate';
      setPendingText('Creating grant slate (check MetaMask)...');
      const slate: any = await submitGrantSlate(requestIDs, slateMetadataHash);
      console.log('Submitted slate', slate);

      // Add slate to db
      const slateToSave: ISaveSlate = {
        slateID: slate.slateID,
        metadataHash: slateMetadataHash,
        email: values.email,
        proposalInfo,
      };

      setPendingText('Saving slate...');
      // api should handle updating, not just adding
      const response = await postSlate(slateToSave);
      if (response.status === 200) {
        console.log('Saved slate info');
        toast.success('Saved slate');

        // stake immediately after creating slate
        if (values.stake === 'yes') {
          errorMessagePrefix = 'error staking on slate';
          if (panBalance.lt(votingRights)) {
            setTxsPending(4);
            setPendingText(
              'Not enough balance. Withdrawing voting rights first (check MetaMask)...'
            );
            await contracts.gatekeeper.withdrawVoteTokens(votingRights);
          }
          if (gkAllowance.lt(slateStakeAmount)) {
            setPendingText('Approving the Gatekeeper to stake on slate (check MetaMask)...');
            await contracts.token.approve(contracts.gatekeeper.address, MaxUint256);
          }
          setPendingText('Staking on slate (check MetaMask)...');
          const res = await sendStakeTokensTransaction(contracts.gatekeeper, slate.slateID);

          await res.wait();
        }

        setTxsPending(0);
        setOpenModal(true);
        onRefreshSlates();
        onRefreshCurrentBallot();
        onRefreshBalances();
      } else {
        errorMessagePrefix = `problem saving slate info ${response.data}`;
        throw new Error(`API error ${response.data}`);
      }
    } catch (error) {
      const fullErrorMessage = `${errorMessagePrefix}: ${error.message}`;
      handleSubmissionError(fullErrorMessage, error);
    }
  }

  const initialValues =
    process.env.NODE_ENV === 'development'
      ? {
          email: '[email protected]',
          firstName: 'Guy',
          lastName: 'Reid',
          organization: 'Panvala',
          description: 'Only the best proposals',
          recommendation: query && query.id ? 'grant' : '',
          proposals: query && query.id ? { [query.id.toString()]: true } : {},
          selectedProposals: [],
          stake: 'no',
        }
      : {
          email: '',
          firstName: '',
          lastName: '',
          organization: '',
          description: '',
          recommendation: query && query.id ? 'grant' : '',
          proposals: query && query.id ? { [query.id.toString()]: true } : {},
          selectedProposals: [],
          stake: 'no',
        };

  if (pageStatus === PageStatus.Loading || pageStatus === PageStatus.Initialized) {
    return <div>Loading...</div>;
  }

  return pageStatus === PageStatus.SubmissionOpen ? (
    <div>
      <Modal handleClick={() => setOpenModal(false)} isOpen={isOpen}>
        <>
          <Image src="/static/check.svg" alt="slate submitted" width="80px" />
          <ModalTitle>{'Slate submitted.'}</ModalTitle>
          <ModalDescription className="flex flex-wrap">
            Now that your slate has been created you and others have the ability to stake tokens on
            it to propose it to token holders. Once there are tokens staked on the slate it will be
            eligible for a vote.
          </ModalDescription>
          <RouterLink href="/slates" as="/slates">
            <Button type="default">{'Done'}</Button>
          </RouterLink>
        </>
      </Modal>

      <CenteredTitle title="Create a Grant Slate" />
      <CenteredWrapper>
        <Formik
          initialValues={initialValues}
          validationSchema={FormSchema}
          onSubmit={async (values: IFormValues, { setSubmitting, setFieldError }: any) => {
            const emptySlate = values.recommendation === 'noAction';

            if (emptySlate) {
              // submit the form values with no proposals
              await handleSubmitSlate(values, []);
            } else {
              const selectedProposalIDs: string[] = Object.keys(values.proposals).filter(
                (p: string) => values.proposals[p] === true
              );

              // validate for at least 1 selected proposal
              if (selectedProposalIDs.length === 0) {
                setFieldError('proposals', 'select at least 1 proposal.');
              } else if (proposals && proposals.length) {
                // filter for only the selected proposal objects
                const selectedProposals: IProposal[] = proposals.filter((p: IProposal) =>
                  selectedProposalIDs.includes(p.id.toString())
                );

                const totalTokens = selectedProposals.reduce((acc, val) => {
                  return utils
                    .bigNumberify(acc)
                    .add(convertedToBaseUnits(val.tokensRequested))
                    .toString();
                }, '0');
                if (
                  contracts.tokenCapacitor.functions.hasOwnProperty('projectedUnlockedBalance') &&
                  utils.bigNumberify(totalTokens).gt(availableTokens)
                ) {
                  setFieldError(
                    'proposals',
                    `token amount exceeds the projected available tokens (${utils.commify(
                      baseToConvertedUnits(availableTokens)
                    )})`
                  );
                } else {
                  // submit the associated proposals along with the slate form values
                  await handleSubmitSlate(values, selectedProposals);
                }
              }
            }

            // re-enable submit button
            setSubmitting(false);
          }}
        >
          {({ isSubmitting, setFieldValue, values, handleSubmit }: FormikContext<IFormValues>) => (
            <Box>
              <Form>
                <PaddedDiv>
                  <SectionLabel>{'ABOUT'}</SectionLabel>

                  <FieldText required label={'Email'} name="email" placeholder="Enter your email" />

                  <FieldText
                    required
                    label={'First Name'}
                    name="firstName"
                    placeholder="Enter your first name"
                  />
                  <FieldText
                    label={'Last Name'}
                    name="lastName"
                    placeholder="Enter your last name"
                  />
                  <FieldText
                    label={'Organization Name'}
                    name="organization"
                    placeholder="Enter your organization's name"
                  />

                  <FieldTextarea
                    required
                    label={'Description'}
                    name="description"
                    placeholder="Enter a description for your slate"
                  />
                </PaddedDiv>
                <Separator />
                <PaddedDiv>
                  <SectionLabel>{'RECOMMENDATION'}</SectionLabel>
                  <Label htmlFor="recommendation" required>
                    {'What type of recommendation would you like to make?'}
                  </Label>
                  <ErrorMessage name="recommendation" component="span" />
                  <div>
                    <Checkbox
                      name="recommendation"
                      value="grant"
                      label="Recommend grant proposals"
                    />
                    <Checkbox name="recommendation" value="noAction" label="Recommend no action" />
                  </div>
                  <RadioSubText>
                    By recommending no action you are opposing any current or future slates for this
                    batch.
                  </RadioSubText>
                </PaddedDiv>
                {values.recommendation === 'grant' && (
                  <>
                    <Separator />
                    <PaddedDiv>
                      <SectionLabel>{'GRANTS'}</SectionLabel>
                      <Label htmlFor="proposals" required>
                        {'Select the grants that you would like to add to your slate'}
                      </Label>
                      <ErrorMessage name="proposals" component="span" />

                      {contracts.tokenCapacitor.functions.hasOwnProperty(
                        'projectedUnlockedBalance'
                      ) && (
                        <Text fontSize="0.75rem" color="grey">
                          {`(There are currently `}
                          <strong>{`${utils.commify(
                            baseToConvertedUnits(availableTokens)
                          )} PAN tokens available`}</strong>
                          {` for grant proposals at this time.)`}
                        </Text>
                      )}

                      <FlexContainer>
                        {proposals &&
                          proposals.map((proposal: IProposal) => (
                            <Card
                              key={proposal.id}
                              category={`${proposal.category} PROPOSAL`}
                              title={proposal.title}
                              subtitle={proposal.tokensRequested.toString()}
                              description={proposal.summary}
                              onClick={() => {
                                if (values.proposals.hasOwnProperty(proposal.id)) {
                                  setFieldValue(
                                    `proposals.${proposal.id}`,
                                    !values.proposals[proposal.id]
                                  );
                                } else {
                                  setFieldValue(`proposals.${proposal.id}`, true);
                                }
                              }}
                              isActive={values.proposals[proposal.id]}
                              type={PROPOSAL}
                              width={['98%', '47%']}
                            />
                          ))}
                      </FlexContainer>
                    </PaddedDiv>
                  </>
                )}
                <Separator />
                <PaddedDiv>
                  <SectionLabel>STAKE</SectionLabel>
                  <Label htmlFor="stake" required>
                    {`Would you like to stake ${formatPanvalaUnits(
                      slateStakeAmount
                    )} tokens for this slate? This makes your slate eligible for the current batch.`}
                  </Label>
                  <ErrorMessage name="stake" component="span" />
                  <div>
                    <Checkbox name="stake" value="yes" label="Yes" />
                    <RadioSubText>
                      By selecting yes, you will stake tokens for your own slate and not have to
                      rely on others to stake tokens for you.
                    </RadioSubText>
                    <Checkbox name="stake" value="no" label="No" />
                    <RadioSubText>
                      By selecting no, you will have to wait for others to stake tokens for your
                      slate or you can stake tokens after you have created the slate.
                    </RadioSubText>
                  </div>
                </PaddedDiv>
                <Separator />
              </Form>
              <Flex p={4} justifyEnd>
                <BackButton />
                <Button type="submit" large primary disabled={isSubmitting} onClick={handleSubmit}>
                  {'Create Slate'}
                </Button>
              </Flex>
              <Loader
                isOpen={txsPending > 0}
                setOpen={() => setTxsPending(0)}
                numTxs={txsPending}
                pendingText={pendingText}
              />
            </Box>
          )}
        </Formik>
      </CenteredWrapper>
    </div>
  ) : (
    <ClosedSlateSubmission deadline={deadline} category={'grant'} />
  );
}