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 |
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 |
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 |
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'} />
);
}