zcash-grant-system/frontend/client/modules/create/utils.ts

256 lines
7.3 KiB
TypeScript
Raw Normal View History

import { ProposalDraft, CreateMilestone } from 'types';
2018-10-04 21:27:02 -07:00
import { TeamMember } from 'types';
import { isValidEthAddress, getAmountError } from 'utils/validators';
2018-10-04 21:27:02 -07:00
import { MILESTONE_STATE, ProposalWithCrowdFund } from 'types';
import { ProposalContractData } from 'modules/web3/actions';
import { Wei, toWei } from 'utils/units';
import { ONE_DAY } from 'utils/time';
import { PROPOSAL_CATEGORY } from 'api/constants';
// TODO: Raise this limit
export const TARGET_ETH_LIMIT = 10;
interface CreateFormErrors {
title?: string;
brief?: string;
category?: string;
target?: string;
team?: string[];
details?: string;
payoutAddress?: string;
trustees?: string[];
milestones?: string[];
deadlineDuration?: string;
voteDuration?: string;
}
export type KeyOfForm = keyof Partial<ProposalDraft>;
export const FIELD_NAME_MAP: { [key in KeyOfForm]: string } = {
title: 'Title',
brief: 'Brief',
category: 'Category',
target: 'Target amount',
team: 'Team',
details: 'Details',
payoutAddress: 'Payout address',
trustees: 'Trustees',
milestones: 'Milestones',
deadlineDuration: 'Funding deadline',
voteDuration: 'Milestone deadline',
// Unused, but required by the type definition
proposalId: '',
dateCreated: '',
stage: '',
};
export function getCreateErrors(
form: Partial<ProposalDraft>,
skipRequired?: boolean,
): CreateFormErrors {
const errors: CreateFormErrors = {};
const { title, team, milestones, target, payoutAddress, trustees } = form;
// Required fields with no extra validation
if (!skipRequired) {
for (const key in form) {
if (!form[key as KeyOfForm]) {
(errors as any)[key as KeyOfForm] = `${
FIELD_NAME_MAP[key as KeyOfForm]
} is required`;
}
}
if (!milestones || !milestones.length) {
errors.milestones = ['Must have at least one milestone'];
}
if (!team || !team.length) {
errors.team = ['Must have at least one team member'];
}
}
// Title
if (title && title.length > 60) {
errors.title = 'Title can only be 60 characters maximum';
}
// Amount to raise
const targetFloat = target ? parseFloat(target) : 0;
if (target && !Number.isNaN(targetFloat)) {
const targetErr = getAmountError(targetFloat, TARGET_ETH_LIMIT);
if (targetErr) {
errors.target = targetErr;
}
}
// Payout address
if (payoutAddress && !isValidEthAddress(payoutAddress)) {
errors.payoutAddress = 'That doesnt look like a valid address';
}
// Trustees
if (trustees) {
let didTrusteeError = false;
const trusteeErrors = trustees.map((address, idx) => {
if (!address) {
return '';
}
let err = '';
if (!isValidEthAddress(address)) {
err = 'That doesnt look like a valid address';
} else if (trustees.indexOf(address) !== idx) {
err = 'That address is already a trustee';
} else if (payoutAddress === address) {
err = 'That address is already a trustee';
}
didTrusteeError = didTrusteeError || !!err;
return err;
});
if (didTrusteeError) {
errors.trustees = trusteeErrors;
}
}
// Milestones
if (milestones) {
let didMilestoneError = false;
let cumulativeMilestonePct = 0;
const milestoneErrors = milestones.map((ms, idx) => {
if (!ms.title || !ms.content || !ms.dateEstimated || !ms.payoutPercent) {
didMilestoneError = true;
return '';
}
let err = '';
if (ms.title.length > 40) {
err = 'Title length can only be 40 characters maximum';
} else if (ms.content.length > 200) {
err = 'Description can only be 200 characters maximum';
}
// Last one shows percentage errors
cumulativeMilestonePct += parseInt(ms.payoutPercent, 10);
if (idx === milestones.length - 1 && cumulativeMilestonePct !== 100) {
err = `Payout percentages doesnt add up to 100% (currently ${cumulativeMilestonePct}%)`;
}
didMilestoneError = didMilestoneError || !!err;
return err;
});
if (didMilestoneError) {
errors.milestones = milestoneErrors;
}
}
// Team
if (team) {
let didTeamError = false;
const teamErrors = team.map(u => {
if (!u.name || !u.title || !u.emailAddress || !u.ethAddress) {
didTeamError = true;
return '';
}
const err = getCreateTeamMemberError(u);
didTeamError = didTeamError || !!err;
return err;
});
if (didTeamError) {
errors.team = teamErrors;
}
}
return errors;
}
export function getCreateTeamMemberError(user: TeamMember) {
if (user.name.length > 30) {
return 'Display name can only be 30 characters maximum';
} else if (user.title.length > 30) {
return 'Title can only be 30 characters maximum';
} else if (!/.+\@.+\..+/.test(user.emailAddress)) {
return 'That doesnt look like a valid email address';
} else if (!isValidEthAddress(user.ethAddress)) {
return 'That doesnt look like a valid ETH address';
}
return '';
}
2018-10-04 21:27:02 -07:00
function milestoneToMilestoneAmount(milestone: CreateMilestone, raiseGoal: Wei) {
return raiseGoal.divn(100).mul(Wei(milestone.payoutPercent));
}
export function proposalToContractData(form: ProposalDraft): ProposalContractData {
const targetInWei = toWei(form.target, 'ether');
const milestoneAmounts = form.milestones.map(m =>
milestoneToMilestoneAmount(m, targetInWei),
);
const immediateFirstMilestonePayout = form.milestones[0]!.immediatePayout;
return {
ethAmount: targetInWei,
payoutAddress: form.payoutAddress,
trusteesAddresses: form.trustees,
milestoneAmounts,
durationInMinutes: form.deadlineDuration || ONE_DAY * 60,
milestoneVotingPeriodInMinutes: form.voteDuration || ONE_DAY * 7,
immediateFirstMilestonePayout,
};
}
// This is kind of a disgusting function, sorry.
export function makeProposalPreviewFromDraft(
draft: ProposalDraft,
): ProposalWithCrowdFund {
const target = parseFloat(draft.target);
return {
proposalId: 0,
proposalUrlId: '0-title',
proposalAddress: '0x0',
dateCreated: Date.now(),
title: draft.title,
body: draft.details,
stage: 'preview',
category: draft.category || PROPOSAL_CATEGORY.DAPP,
team: draft.team,
milestones: draft.milestones.map((m, idx) => ({
index: idx,
title: m.title,
body: m.content,
content: m.content,
amount: toWei(target * (parseInt(m.payoutPercent, 10) / 100), 'ether'),
amountAgainstPayout: Wei('0'),
percentAgainstPayout: 0,
payoutRequestVoteDeadline: Date.now(),
dateEstimated: m.dateEstimated,
immediatePayout: m.immediatePayout,
isImmediatePayout: m.immediatePayout,
isPaid: false,
payoutPercent: m.payoutPercent.toString(),
state: MILESTONE_STATE.WAITING,
stage: MILESTONE_STATE.WAITING,
})),
crowdFund: {
immediateFirstMilestonePayout: draft.milestones[0].immediatePayout,
balance: Wei('0'),
funded: Wei('0'),
percentFunded: 0,
target: toWei(target, 'ether'),
amountVotingForRefund: Wei('0'),
percentVotingForRefund: 0,
beneficiary: draft.payoutAddress,
trustees: draft.trustees,
deadline: Date.now() + 100000,
contributors: [],
milestones: [],
milestoneVotingPeriod: 0,
isFrozen: false,
isRaiseGoalReached: false,
},
crowdFundContract: null,
};
}