Merge pull request #33 from dternyak/proposal-rework-amounts

Proposal rework amounts
This commit is contained in:
Daniel Ternyak 2019-01-07 19:09:33 -06:00 committed by GitHub
commit a0d115a703
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 207 additions and 279 deletions

View File

@ -236,6 +236,7 @@ class ProposalSchema(ma.Schema):
"brief", "brief",
"proposal_id", "proposal_id",
"target", "target",
"funded",
"content", "content",
"comments", "comments",
"updates", "updates",
@ -250,6 +251,7 @@ class ProposalSchema(ma.Schema):
date_created = ma.Method("get_date_created") date_created = ma.Method("get_date_created")
proposal_id = ma.Method("get_proposal_id") proposal_id = ma.Method("get_proposal_id")
funded = ma.Method("get_funded")
comments = ma.Nested("CommentSchema", many=True) comments = ma.Nested("CommentSchema", many=True)
updates = ma.Nested("ProposalUpdateSchema", many=True) updates = ma.Nested("ProposalUpdateSchema", many=True)
@ -264,6 +266,10 @@ class ProposalSchema(ma.Schema):
def get_date_created(self, obj): def get_date_created(self, obj):
return dt_to_unix(obj.date_created) return dt_to_unix(obj.date_created)
def get_funded(self, obj):
# TODO: Add up all contributions and return that
return "0"
proposal_schema = ProposalSchema() proposal_schema = ProposalSchema()
proposals_schema = ProposalSchema(many=True) proposals_schema = ProposalSchema(many=True)

View File

@ -34,7 +34,7 @@ export default class AddressInput extends React.Component<Props> {
}; };
const inputProps = { const inputProps = {
placeholder: DONATION.ETH, placeholder: DONATION.ZCASH_SPROUT,
prefix: value && prefix: value &&
showIdenticon && ( showIdenticon && (
<Identicon className="AddressInput-input-identicon" address={value} /> <Identicon className="AddressInput-input-identicon" address={value} />

View File

@ -111,7 +111,7 @@ export default class CreateFlowBasics extends React.Component<Props, State> {
type="number" type="number"
value={target} value={target}
onChange={this.handleInputChange} onChange={this.handleInputChange}
addonAfter="ETH" addonAfter="ZEC"
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -40,7 +40,7 @@ export default class CreateFlowPayment extends React.Component<Props, State> {
<Input <Input
size="large" size="large"
name="payoutAddress" name="payoutAddress"
placeholder={DONATION.ETH} placeholder={DONATION.ZCASH_SPROUT}
type="text" type="text"
value={payoutAddress} value={payoutAddress}
onChange={this.handleInputChange} onChange={this.handleInputChange}

View File

@ -64,7 +64,7 @@ class CreateReview extends React.Component<Props> {
}, },
{ {
key: 'target', key: 'target',
content: <div style={{ fontSize: '1.2rem' }}>{form.target} ETH</div>, content: <div style={{ fontSize: '1.2rem' }}>{form.target} ZEC</div>,
error: errors.target, error: errors.target,
}, },
], ],

View File

@ -21,9 +21,9 @@ export default class Profile extends React.Component<OwnProps> {
</Link> </Link>
<div className="ProfileProposal-brief">{brief}</div> <div className="ProfileProposal-brief">{brief}</div>
<div className="ProfileProposal-raised"> <div className="ProfileProposal-raised">
<UnitDisplay value={funded} symbol="ETH" displayShortBalance={4} />{' '} <UnitDisplay value={funded} symbol="ZEC" displayShortBalance={4} />{' '}
<small>raised</small> of{' '} <small>raised</small> of{' '}
<UnitDisplay value={target} symbol="ETH" displayShortBalance={4} /> goal <UnitDisplay value={target} symbol="ZEC" displayShortBalance={4} /> goal
</div> </div>
</div> </div>
<div className="ProfileProposal-block"> <div className="ProfileProposal-block">

View File

@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import BN from 'bn.js';
import { Spin, Form, Input, Button, Icon } from 'antd'; import { Spin, Form, Input, Button, Icon } from 'antd';
import { Proposal } from 'types'; import { Proposal } from 'types';
import classnames from 'classnames'; import classnames from 'classnames';
import { fromWei } from 'utils/units'; import { fromZat } from 'utils/units';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { compose } from 'recompose'; import { compose } from 'recompose';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import ShortAddress from 'components/ShortAddress';
import UnitDisplay from 'components/UnitDisplay'; import UnitDisplay from 'components/UnitDisplay';
import { getAmountError } from 'utils/validators'; import { getAmountError } from 'utils/validators';
import { CATEGORY_UI } from 'api/constants'; import { CATEGORY_UI } from 'api/constants';
@ -51,8 +49,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
} }
// TODO: Get values from proposal // TODO: Get values from proposal
const target = new BN(0); const { target, funded } = this.props.proposal;
const funded = new BN(0);
const remainingTarget = target.sub(funded); const remainingTarget = target.sub(funded);
const amount = parseFloat(value); const amount = parseFloat(value);
let amountError = null; let amountError = null;
@ -60,8 +57,8 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
if (Number.isNaN(amount)) { if (Number.isNaN(amount)) {
// They're entering some garbage, theyll work it out // They're entering some garbage, theyll work it out
} else { } else {
const remainingEthNum = parseFloat(fromWei(remainingTarget, 'ether')); const remainingTargetNum = parseFloat(fromZat(remainingTarget));
amountError = getAmountError(amount, remainingEthNum); amountError = getAmountError(amount, remainingTargetNum);
} }
this.setState({ amountToRaise: value, amountError }); this.setState({ amountToRaise: value, amountError });
@ -80,22 +77,16 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
const amountFloat = parseFloat(amountToRaise) || 0; const amountFloat = parseFloat(amountToRaise) || 0;
let content; let content;
if (proposal) { if (proposal) {
const { target, funded, percentFunded } = proposal;
const isRaiseGoalReached = funded.gte(target);
// TODO: Get values from proposal // TODO: Get values from proposal
console.warn('TODO: Get real values from proposal for CampaignBlock'); console.warn('TODO: Get deadline and isFrozen from proposal data');
const isRaiseGoalReached = false;
const deadline = 0; const deadline = 0;
const isFrozen = false; const isFrozen = false;
const target = new BN(0);
const funded = new BN(0);
const percentFunded = 0;
const beneficiary = 'z123';
const isFundingOver = const isFundingOver = isRaiseGoalReached || deadline < Date.now() || isFrozen;
isRaiseGoalReached ||
deadline < Date.now() ||
isFrozen;
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview; const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
const remainingEthNum = parseFloat(fromWei(target.sub(funded), 'ether')); const remainingTargetNum = parseFloat(fromZat(target.sub(funded)));
content = ( content = (
<React.Fragment> <React.Fragment>
@ -123,17 +114,10 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
</div> </div>
</div> </div>
)} )}
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Beneficiary</div>
<div className="ProposalCampaignBlock-info-value">
<ShortAddress address={beneficiary} />
</div>
</div>
<div className="ProposalCampaignBlock-info"> <div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Funding</div> <div className="ProposalCampaignBlock-info-label">Funding</div>
<div className="ProposalCampaignBlock-info-value"> <div className="ProposalCampaignBlock-info-value">
<UnitDisplay value={funded} /> /{' '} <UnitDisplay value={funded} /> / <UnitDisplay value={target} symbol="ZEC" />
<UnitDisplay value={target} symbol="ETH" />
</div> </div>
</div> </div>
@ -175,7 +159,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
size="large" size="large"
type="number" type="number"
placeholder="0.5" placeholder="0.5"
addonAfter="ETH" addonAfter="ZEC"
disabled={true} disabled={true}
/> />
</Form.Item> </Form.Item>
@ -193,10 +177,10 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
value={amountToRaise} value={amountToRaise}
placeholder="0.5" placeholder="0.5"
min={0} min={0}
max={remainingEthNum} max={remainingTargetNum}
step={0.1} step={0.1}
onChange={this.handleAmountChange} onChange={this.handleAmountChange}
addonAfter="ETH" addonAfter="ZEC"
disabled={isPreview} disabled={isPreview}
/> />
</Form.Item> </Form.Item>

View File

@ -15,7 +15,9 @@ const ContributorsBlock = () => {
<AddressRow <AddressRow
key={contributor.address} key={contributor.address}
address={contributor.address} address={contributor.address}
secondary={<UnitDisplay value={contributor.contributionAmount} symbol="ETH" />} secondary={
<UnitDisplay value={contributor.contributionAmount} symbol="ZEC" />
}
/> />
)); ));
} else { } else {

View File

@ -1,7 +1,6 @@
import lodash from 'lodash'; import lodash from 'lodash';
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import BN from 'bn.js';
import { Alert, Steps, Spin } from 'antd'; import { Alert, Steps, Spin } from 'antd';
import { Proposal, MILESTONE_STATE } from 'types'; import { Proposal, MILESTONE_STATE } from 'types';
import UnitDisplay from 'components/UnitDisplay'; import UnitDisplay from 'components/UnitDisplay';
@ -98,11 +97,8 @@ class ProposalMilestones extends React.Component<Props, State> {
const className = this.state.step === i ? 'is-active' : 'is-inactive'; const className = this.state.step === i ? 'is-active' : 'is-inactive';
const estimatedDate = moment(milestone.dateEstimated).format('MMMM YYYY'); const estimatedDate = moment(milestone.dateEstimated).format('MMMM YYYY');
// TODO: Real milestone amount
console.warn('TODO: Real milestone amount in Proposal/Milestones/index.tsx');
const amount = new BN(0);
const reward = ( const reward = (
<UnitDisplay value={amount} symbol="ETH" displayShortBalance={4} /> <UnitDisplay value={milestone.amount} symbol="ZEC" displayShortBalance={4} />
); );
const alertStyle = { width: 'fit-content', margin: '0 0 1rem 0' }; const alertStyle = { width: 'fit-content', margin: '0 0 1rem 0' };
@ -125,9 +121,8 @@ class ProposalMilestones extends React.Component<Props, State> {
The team was awarded <strong>{reward}</strong>{' '} The team was awarded <strong>{reward}</strong>{' '}
{milestone.isImmediatePayout {milestone.isImmediatePayout
? 'as an initial payout' ? 'as an initial payout'
: `on ${moment(milestone.payoutRequestVoteDeadline).format( // TODO: Add property for payout date on milestones
'MMM Do, YYYY', : `on ${moment().format('MMM Do, YYYY')}`}
)}`}
. .
</span> </span>
} }
@ -139,12 +134,10 @@ class ProposalMilestones extends React.Component<Props, State> {
notification = ( notification = (
<Alert <Alert
type="info" type="info"
message={ message={`
<span> The team has requested a payout for this milestone. It is
Payout vote is in progress! The approval period ends{' '} currently under review.
{moment(milestone.payoutRequestVoteDeadline).from(new Date())}. `}
</span>
}
style={alertStyle} style={alertStyle}
/> />
); );
@ -155,10 +148,11 @@ class ProposalMilestones extends React.Component<Props, State> {
type="warning" type="warning"
message={ message={
<span> <span>
Payout was voted against on{' '} Payout for this milestone was rejected on{' '}
{moment(milestone.payoutRequestVoteDeadline).format('MMM Do, YYYY')}. {/* TODO: add property for payout rejection date on milestones */}
{isTrustee ? ' You ' : ' The team '} can request another payout vote at {moment().format('MMM Do, YYYY')}.
any time. {isTrustee ? ' You ' : ' The team '} can request another
review for payout at any time.
</span> </span>
} }
style={alertStyle} style={alertStyle}

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import BN from 'bn.js';
import { Progress, Icon } from 'antd'; import { Progress, Icon } from 'antd';
import moment from 'moment'; import moment from 'moment';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
@ -23,12 +22,10 @@ export class ProposalCard extends React.Component<Proposal> {
category, category,
dateCreated, dateCreated,
team, team,
target,
funded,
percentFunded,
} = this.props; } = this.props;
// TODO: Real values from proposal
console.warn('TODO: Real values for ProposalCard');
const target = new BN(0);
const funded = new BN(0);
const percentFunded = 0;
return ( return (
<div <div
@ -38,8 +35,8 @@ export class ProposalCard extends React.Component<Proposal> {
<h3 className="ProposalCard-title">{title}</h3> <h3 className="ProposalCard-title">{title}</h3>
<div className="ProposalCard-funding"> <div className="ProposalCard-funding">
<div className="ProposalCard-funding-raised"> <div className="ProposalCard-funding-raised">
<UnitDisplay value={funded} symbol="ETH" /> <small>raised</small> of{' '} <UnitDisplay value={funded} symbol="ZEC" /> <small>raised</small> of{' '}
<UnitDisplay value={target} symbol="ETH" /> goal <UnitDisplay value={target} symbol="ZEC" /> goal
</div> </div>
<div <div
className={classnames({ className={classnames({

View File

@ -1,23 +1,28 @@
// Adapted from https://github.com/MyCryptoHQ/MyCrypto/blob/develop/common/components/ui/UnitDisplay.tsx // Adapted from https://github.com/MyCryptoHQ/MyCrypto/blob/develop/common/components/ui/UnitDisplay.tsx
import React from 'react'; import React from 'react';
import { import {
fromTokenBase, baseToConvertedUnit,
getDecimalFromEtherUnit, getDecimalFromUnitKey,
UnitKey, UnitKey,
Wei, Zat,
TokenValue,
} from 'utils/units'; } from 'utils/units';
import { formatNumber } from 'utils/formatters'; import { formatNumber } from 'utils/formatters';
interface Props { interface Props {
/** /**
* @description base value of the token / ether, incase of waiting for API calls, we can return '???' * @description base value of the unit
* @type {TokenValue | Wei} * @type {Zat}
* @memberof Props * @memberof Props
*/ */
value: TokenValue | Wei; value: Zat;
/** /**
* @description Symbol to display to the right of the value, such as 'ETH' * @description Name of the unit to display, defaults to 'zcash'
* @type {UnitKey}
* @memberof Props
*/
unit?: UnitKey;
/**
* @description Symbol to display to the right of the value, such as 'ZEC'
* @type {string} * @type {string}
* @memberof Props * @memberof Props
*/ */
@ -32,22 +37,13 @@ interface Props {
checkOffline?: boolean; checkOffline?: boolean;
} }
interface EthProps extends Props { const UnitDisplay: React.SFC<Props> = params => {
unit?: UnitKey;
}
interface TokenProps extends Props {
decimal: number;
}
const isTokenUnit = (param: EthProps | TokenProps): param is TokenProps =>
!!(param as TokenProps).decimal;
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance, displayTrailingZeroes } = params; const { value, symbol, displayShortBalance, displayTrailingZeroes } = params;
const convertedValue = isTokenUnit(params) const convertedValue = baseToConvertedUnit(
? fromTokenBase(value, params.decimal) value.toString(),
: fromTokenBase(value, getDecimalFromEtherUnit(params.unit || 'ether')); getDecimalFromUnitKey(params.unit || 'zcash'),
);
let formattedValue; let formattedValue;

View File

@ -2,12 +2,11 @@ import { ProposalDraft, CreateMilestone } from 'types';
import { User } from 'types'; import { User } from 'types';
import { getAmountError } from 'utils/validators'; import { getAmountError } from 'utils/validators';
import { MILESTONE_STATE, Proposal } from 'types'; import { MILESTONE_STATE, Proposal } from 'types';
import { Wei, toWei } from 'utils/units'; import { Zat, toZat } from 'utils/units';
import { ONE_DAY } from 'utils/time'; import { ONE_DAY } from 'utils/time';
import { PROPOSAL_CATEGORY } from 'api/constants'; import { PROPOSAL_CATEGORY } from 'api/constants';
// TODO: Raise this limit export const TARGET_ZEC_LIMIT = 1000;
export const TARGET_ETH_LIMIT = 1000;
interface CreateFormErrors { interface CreateFormErrors {
title?: string; title?: string;
@ -75,7 +74,7 @@ export function getCreateErrors(
// Amount to raise // Amount to raise
const targetFloat = target ? parseFloat(target) : 0; const targetFloat = target ? parseFloat(target) : 0;
if (target && !Number.isNaN(targetFloat)) { if (target && !Number.isNaN(targetFloat)) {
const targetErr = getAmountError(targetFloat, TARGET_ETH_LIMIT); const targetErr = getAmountError(targetFloat, TARGET_ZEC_LIMIT);
if (targetErr) { if (targetErr) {
errors.target = targetErr; errors.target = targetErr;
} }
@ -148,19 +147,19 @@ export function getCreateWarnings(form: Partial<ProposalDraft>): string[] {
return warnings; return warnings;
} }
function milestoneToMilestoneAmount(milestone: CreateMilestone, raiseGoal: Wei) { function milestoneToMilestoneAmount(milestone: CreateMilestone, raiseGoal: Zat) {
return raiseGoal.divn(100).mul(Wei(milestone.payoutPercent)); return raiseGoal.divn(100).mul(Zat(milestone.payoutPercent));
} }
export function proposalToContractData(form: ProposalDraft): any { export function proposalToContractData(form: ProposalDraft): any {
const targetInWei = toWei(form.target, 'ether'); const targetInZat = toZat(form.target);
const milestoneAmounts = form.milestones.map(m => const milestoneAmounts = form.milestones.map(m =>
milestoneToMilestoneAmount(m, targetInWei), milestoneToMilestoneAmount(m, targetInZat),
); );
const immediateFirstMilestonePayout = form.milestones[0]!.immediatePayout; const immediateFirstMilestonePayout = form.milestones[0]!.immediatePayout;
return { return {
ethAmount: targetInWei, ethAmount: targetInZat,
payoutAddress: form.payoutAddress, payoutAddress: form.payoutAddress,
trusteesAddresses: [], trusteesAddresses: [],
milestoneAmounts, milestoneAmounts,
@ -180,10 +179,15 @@ export function makeProposalPreviewFromDraft(
proposalId: 0, proposalId: 0,
proposalUrlId: '0-title', proposalUrlId: '0-title',
proposalAddress: '0x0', proposalAddress: '0x0',
payoutAddress: '0x0',
dateCreated: Date.now(), dateCreated: Date.now(),
title: draft.title, title: draft.title,
brief: draft.brief, brief: draft.brief,
content: draft.content, content: draft.content,
deadlineDuration: 86400 * 60,
target: toZat(draft.target),
funded: Zat('0'),
percentFunded: 0,
stage: 'preview', stage: 'preview',
category: draft.category || PROPOSAL_CATEGORY.DAPP, category: draft.category || PROPOSAL_CATEGORY.DAPP,
team: draft.team, team: draft.team,
@ -191,17 +195,13 @@ export function makeProposalPreviewFromDraft(
index: idx, index: idx,
title: m.title, title: m.title,
content: m.content, content: m.content,
amount: toWei(target * (parseInt(m.payoutPercent, 10) / 100), 'ether'), amount: toZat(target * (parseInt(m.payoutPercent, 10) / 100)),
amountAgainstPayout: Wei('0'),
percentAgainstPayout: 0,
payoutRequestVoteDeadline: Date.now(),
dateEstimated: m.dateEstimated, dateEstimated: m.dateEstimated,
immediatePayout: m.immediatePayout, immediatePayout: m.immediatePayout,
isImmediatePayout: m.immediatePayout, isImmediatePayout: m.immediatePayout,
isPaid: false, isPaid: false,
payoutPercent: m.payoutPercent.toString(), payoutPercent: m.payoutPercent.toString(),
state: MILESTONE_STATE.WAITING, state: MILESTONE_STATE.WAITING,
stage: MILESTONE_STATE.WAITING,
})), })),
}; };
} }

1
frontend/client/typings/omit.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

View File

@ -1,7 +1,8 @@
import BN from 'bn.js'; import BN from 'bn.js';
import { User, Proposal, UserProposal } from 'types'; import { User, Proposal, UserProposal, MILESTONE_STATE } from 'types';
import { UserState } from 'modules/users/reducers'; import { UserState } from 'modules/users/reducers';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { toZat } from './units';
export function formatUserForPost(user: User) { export function formatUserForPost(user: User) {
return { return {
@ -21,8 +22,22 @@ export function formatUserFromGet(user: UserState) {
return user; return user;
} }
export function formatProposalFromGet(proposal: Proposal) { export function formatProposalFromGet(p: any): Proposal {
const proposal = { ...p } as Proposal;
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title); proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
proposal.target = toZat(p.target);
proposal.funded = toZat(p.funded);
proposal.percentFunded = proposal.funded.div(proposal.target.divn(100)).toNumber();
proposal.milestones = proposal.milestones.map((m: any, index: number) => {
return {
...m,
index,
amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
// TODO: Get data from backend
state: MILESTONE_STATE.WAITING,
isPaid: false,
};
});
return proposal; return proposal;
} }
@ -48,6 +63,16 @@ export function extractProposalIdFromUrl(slug: string) {
// pre-hydration massage (BNify JSONed BNs) // pre-hydration massage (BNify JSONed BNs)
export function massageSerializedState(state: AppState) { export function massageSerializedState(state: AppState) {
// proposals
state.proposal.proposals = state.proposal.proposals.map(p => ({
...p,
target: new BN((p.target as any) as string, 16),
funded: new BN((p.funded as any) as string, 16),
milestones: p.milestones.map(m => ({
...m,
amount: new BN((m.amount as any) as string, 16),
})),
}));
// users // users
const bnUserProp = (p: UserProposal) => { const bnUserProp = (p: UserProposal) => {
p.funded = new BN(p.funded, 16); p.funded = new BN(p.funded, 16);

View File

@ -1,3 +1,4 @@
export const DONATION = { export const DONATION = {
ETH: '0x8B0B72F8bDE212991135668922fD5acE557DE6aB', ZCASH_TRANSPARENT: 't1aib2cbwPVrFfrjGGkhWD67imdBet1xDTr',
ZCASH_SPROUT: 'zcWGwZU7FyUgpdrWGkeFqCEnvhLRDAVuf2ZbhW4vzNMTTR6VUgfiBGkiNbkC4e38QaPtS13RKZCriqN9VcyyKNRRQxbgnen',
}; };

View File

@ -2,42 +2,16 @@
import BN from 'bn.js'; import BN from 'bn.js';
import { stripHexPrefix } from 'utils/formatters'; import { stripHexPrefix } from 'utils/formatters';
type UnitKey = keyof typeof Units; export const ZCASH_DECIMAL = 8;
type Wei = BN;
type TokenValue = BN;
type Address = Buffer;
type Nonce = BN;
type Data = Buffer;
export const ETH_DECIMAL = 18;
export const Units = { export const Units = {
wei: '1', zat: '1',
kwei: '1000', zcash: '100000000',
ada: '1000',
femtoether: '1000',
mwei: '1000000',
babbage: '1000000',
picoether: '1000000',
gwei: '1000000000',
shannon: '1000000000',
nanoether: '1000000000',
nano: '1000000000',
szabo: '1000000000000',
microether: '1000000000000',
micro: '1000000000000',
finney: '1000000000000000',
milliether: '1000000000000000',
milli: '1000000000000000',
ether: '1000000000000000000',
kether: '1000000000000000000000',
grand: '1000000000000000000000',
einstein: '1000000000000000000000',
mether: '1000000000000000000000000',
gether: '1000000000000000000000000000',
tether: '1000000000000000000000000000000',
}; };
const handleValues = (input: string | BN) => {
export type Zat = BN;
export type UnitKey = keyof typeof Units;
export const handleValues = (input: string | BN) => {
if (typeof input === 'string') { if (typeof input === 'string') {
return input.startsWith('0x') ? new BN(stripHexPrefix(input), 16) : new BN(input); return input.startsWith('0x') ? new BN(stripHexPrefix(input), 16) : new BN(input);
} }
@ -51,20 +25,14 @@ const handleValues = (input: string | BN) => {
} }
}; };
const Nonce = (input: string | BN) => handleValues(input); export const Zat = (input: string | BN): Zat => handleValues(input);
const Wei = (input: string | BN): Wei => handleValues(input);
const TokenValue = (input: string | BN) => handleValues(input);
const getDecimalFromEtherUnit = (key: UnitKey) => Units[key].length - 1;
const stripRightZeros = (str: string) => { const stripRightZeros = (str: string) => {
const strippedStr = str.replace(/0+$/, ''); const strippedStr = str.replace(/0+$/, '');
return strippedStr === '' ? null : strippedStr; return strippedStr === '' ? null : strippedStr;
}; };
const baseToConvertedUnit = (value: string, decimal: number) => { export const baseToConvertedUnit = (value: string, decimal: number) => {
if (decimal === 0) { if (decimal === 0) {
return value; return value;
} }
@ -83,52 +51,14 @@ const convertedToBaseUnit = (value: string, decimal: number) => {
return `${integerPart}${paddedFraction}`; return `${integerPart}${paddedFraction}`;
}; };
const fromWei = (wei: Wei, unit: UnitKey) => { export const fromZat = (zat: Zat) => {
const decimal = getDecimalFromEtherUnit(unit); return baseToConvertedUnit(zat.toString(), ZCASH_DECIMAL);
return baseToConvertedUnit(wei.toString(), decimal);
}; };
const toWei = (value: string | number, unitType: number | UnitKey): Wei => { export const toZat = (value: string | number): Zat => {
value = value.toString(); value = value.toString();
let decimal; const zat = convertedToBaseUnit(value, ZCASH_DECIMAL);
if (typeof unitType === 'string') { return Zat(zat);
decimal = getDecimalFromEtherUnit(unitType);
} else {
decimal = unitType;
}
const wei = convertedToBaseUnit(value, decimal);
return Wei(wei);
}; };
const fromTokenBase = (value: TokenValue, decimal: number) => export const getDecimalFromUnitKey = (key: UnitKey) => Units[key].length - 1;
baseToConvertedUnit(value.toString(), decimal);
const toTokenBase = (value: string, decimal: number) =>
TokenValue(convertedToBaseUnit(value, decimal));
const convertTokenBase = (value: TokenValue, oldDecimal: number, newDecimal: number) => {
if (oldDecimal === newDecimal) {
return value;
}
return toTokenBase(fromTokenBase(value, oldDecimal), newDecimal);
};
const gasPriceToBase = (price: number) =>
toWei(price.toString(), getDecimalFromEtherUnit('gwei'));
export {
Data,
Address,
TokenValue,
fromWei,
toWei,
toTokenBase,
fromTokenBase,
convertTokenBase,
Wei,
getDecimalFromEtherUnit,
UnitKey,
Nonce,
handleValues,
gasPriceToBase,
};

View File

@ -7,7 +7,7 @@ export function getAmountError(amount: number, max: number = Infinity) {
) { ) {
return 'Must be in increments of 0.001'; return 'Must be in increments of 0.001';
} else if (amount > max) { } else if (amount > max) {
return `Cannot exceed maximum (${max} ETH)`; return `Cannot exceed maximum (${max} ZEC)`;
} }
return null; return null;

View File

@ -64,7 +64,7 @@ class Example extends React.Component<ExampleProps, ExampleState> {
constructor(props: ExampleProps) { constructor(props: ExampleProps) {
super(props); super(props);
this.state = { this.state = {
value: props.defaultValue !== undefined ? props.defaultValue : DONATION.ETH, value: props.defaultValue !== undefined ? props.defaultValue : DONATION.ZCASH_SPROUT,
}; };
} }

View File

@ -7,21 +7,21 @@ import Contributors from 'components/Proposal/Contributors';
import 'styles/style.less'; import 'styles/style.less';
import 'components/Proposal/style.less'; import 'components/Proposal/style.less';
import 'components/Proposal/Governance/style.less'; import 'components/Proposal/Governance/style.less';
import { getProposalWithCrowdFund } from './props'; import { generateProposal } from './props';
const propsNoFunding = getProposalWithCrowdFund({ const propsNoFunding = generateProposal({
amount: 5, amount: 5,
funded: 0, funded: 0,
}); });
const propsHalfFunded = getProposalWithCrowdFund({ const propsHalfFunded = generateProposal({
amount: 5, amount: 5,
funded: 2.5, funded: 2.5,
}); });
const propsFunded = getProposalWithCrowdFund({ const propsFunded = generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
}); });
const propsNotFundedExpired = getProposalWithCrowdFund({ const propsNotFundedExpired = generateProposal({
created: Date.now() - 10, created: Date.now() - 10,
deadline: Date.now() - 1, deadline: Date.now() - 1,
}); });

View File

@ -6,21 +6,21 @@ import { ProposalCard } from 'components/Proposals/ProposalCard';
import 'styles/style.less'; import 'styles/style.less';
import 'components/Proposal/style.less'; import 'components/Proposal/style.less';
import 'components/Proposal/Governance/style.less'; import 'components/Proposal/Governance/style.less';
import { getProposalWithCrowdFund } from './props'; import { generateProposal } from './props';
const propsNoFunding = getProposalWithCrowdFund({ const propsNoFunding = generateProposal({
amount: 5, amount: 5,
funded: 0, funded: 0,
}); });
const propsHalfFunded = getProposalWithCrowdFund({ const propsHalfFunded = generateProposal({
amount: 5, amount: 5,
funded: 2.5, funded: 2.5,
}); });
const propsFunded = getProposalWithCrowdFund({ const propsFunded = generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
}); });
const propsNotFundedExpired = getProposalWithCrowdFund({ const propsNotFundedExpired = generateProposal({
created: Date.now() - 1000 * 60 * 60 * 10, created: Date.now() - 1000 * 60 * 60 * 10,
deadline: Date.now() - 1, deadline: Date.now() - 1,
}); });

View File

@ -11,7 +11,7 @@ const { WAITING, ACTIVE, PAID, REJECTED } = MILESTONE_STATE;
import 'styles/style.less'; import 'styles/style.less';
import 'components/Proposal/style.less'; import 'components/Proposal/style.less';
import 'components/Proposal/Governance/style.less'; import 'components/Proposal/Governance/style.less';
import { getProposalWithCrowdFund } from './props'; import { generateProposal } from './props';
const msWaiting = { state: WAITING, isPaid: false }; const msWaiting = { state: WAITING, isPaid: false };
const msPaid = { state: PAID, isPaid: true }; const msPaid = { state: PAID, isPaid: true };
@ -22,96 +22,96 @@ const trustee = 'z123';
const contributor = 'z456'; const contributor = 'z456';
const geometryCases = [...Array(10).keys()].map(i => const geometryCases = [...Array(10).keys()].map(i =>
getProposalWithCrowdFund({ milestoneCount: i + 1 }), generateProposal({ milestoneCount: i + 1 }),
); );
const cases: { [index: string]: any } = { const cases: { [index: string]: any } = {
// trustee - first // trustee - first
['not funded']: getProposalWithCrowdFund({ ['not funded']: generateProposal({
amount: 5, amount: 5,
funded: 0, funded: 0,
}), }),
['first - waiting']: getProposalWithCrowdFund({ ['first - waiting']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
}), }),
['first - not paid']: getProposalWithCrowdFund({ ['first - not paid']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [ milestoneOverrides: [
{ state: PAID, isPaid: false, payoutRequestVoteDeadline: Date.now() }, { state: PAID, isPaid: false },
msWaiting, msWaiting,
msWaiting, msWaiting,
], ],
}), }),
// trustee - second // trustee - second
['second - waiting']: getProposalWithCrowdFund({ ['second - waiting']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msWaiting, msWaiting], milestoneOverrides: [msPaid, msWaiting, msWaiting],
}), }),
['second - active']: getProposalWithCrowdFund({ ['second - active']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msActive, msWaiting], milestoneOverrides: [msPaid, msActive, msWaiting],
}), }),
['second - not paid']: getProposalWithCrowdFund({ ['second - not paid']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [ milestoneOverrides: [
msPaid, msPaid,
{ state: PAID, isPaid: false, payoutRequestVoteDeadline: Date.now() }, { state: PAID, isPaid: false },
msWaiting, msWaiting,
], ],
}), }),
['second - no vote']: getProposalWithCrowdFund({ ['second - no vote']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [ milestoneOverrides: [
msPaid, msPaid,
{ state: ACTIVE, isPaid: false, percentAgainstPayout: 33 }, { state: ACTIVE, isPaid: false },
msWaiting, msWaiting,
], ],
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }], contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
}), }),
['second - rejected']: getProposalWithCrowdFund({ ['second - rejected']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msRejected, msWaiting], milestoneOverrides: [msPaid, msRejected, msWaiting],
}), }),
// trustee - third // trustee - third
['final - waiting']: getProposalWithCrowdFund({ ['final - waiting']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msPaid, msWaiting], milestoneOverrides: [msPaid, msPaid, msWaiting],
}), }),
['final - active']: getProposalWithCrowdFund({ ['final - active']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msPaid, msActive], milestoneOverrides: [msPaid, msPaid, msActive],
}), }),
['final - not paid']: getProposalWithCrowdFund({ ['final - not paid']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [ milestoneOverrides: [
msPaid, msPaid,
msPaid, msPaid,
{ state: PAID, isPaid: false, payoutRequestVoteDeadline: Date.now() }, { state: PAID, isPaid: false },
], ],
}), }),
['final - no vote']: getProposalWithCrowdFund({ ['final - no vote']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [ milestoneOverrides: [
msPaid, msPaid,
msPaid, msPaid,
{ state: ACTIVE, isPaid: false, percentAgainstPayout: 33 }, { state: ACTIVE, isPaid: false },
], ],
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }], contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
}), }),
['final - rejected']: getProposalWithCrowdFund({ ['final - rejected']: generateProposal({
amount: 5, amount: 5,
funded: 5, funded: 5,
milestoneOverrides: [msPaid, msPaid, msRejected], milestoneOverrides: [msPaid, msPaid, msRejected],

View File

@ -4,30 +4,30 @@ import BN from 'bn.js';
import UnitDisplay from '../client/components/UnitDisplay'; import UnitDisplay from '../client/components/UnitDisplay';
const oneEth = new BN('1000000000000000000'); const oneZEC = new BN('1000000000000000000');
const cases = [ const cases = [
{ {
disp: 'basic', disp: 'basic',
props: { value: oneEth.mul(new BN(25)), symbol: 'ETH' }, props: { value: oneZEC.mul(new BN(25)), symbol: 'ZEC' },
}, },
{ {
disp: 'fraction', disp: 'fraction',
props: { value: oneEth.div(new BN(3)), symbol: 'ETH' }, props: { value: oneZEC.div(new BN(3)), symbol: 'ZEC' },
}, },
{ {
disp: 'fraction - displayShortBalance: true', disp: 'fraction - displayShortBalance: true',
props: { value: oneEth.div(new BN(3)), symbol: 'ETH', displayShortBalance: true }, props: { value: oneZEC.div(new BN(3)), symbol: 'ZEC', displayShortBalance: true },
}, },
{ {
disp: 'fraction - displayShortBalance: 2', disp: 'fraction - displayShortBalance: 2',
props: { value: oneEth.div(new BN(3)), symbol: 'ETH', displayShortBalance: 2 }, props: { value: oneZEC.div(new BN(3)), symbol: 'ZEC', displayShortBalance: 2 },
}, },
{ {
disp: 'fraction - displayShortBalance: 4, displayTrailingZeros: false', disp: 'fraction - displayShortBalance: 4, displayTrailingZeros: false',
props: { props: {
value: oneEth.div(new BN(2)), value: oneZEC.div(new BN(2)),
symbol: 'ETH', symbol: 'ZEC',
displayShortBalance: 4, displayShortBalance: 4,
displayTrailingZeroes: false, displayTrailingZeroes: false,
}, },
@ -35,21 +35,21 @@ const cases = [
{ {
disp: 'fraction - displayShortBalance: 4, displayTrailingZeros: true', disp: 'fraction - displayShortBalance: 4, displayTrailingZeros: true',
props: { props: {
value: oneEth.div(new BN(2)), value: oneZEC.div(new BN(2)),
symbol: 'ETH', symbol: 'ZEC',
displayShortBalance: 4, displayShortBalance: 4,
displayTrailingZeroes: true, displayTrailingZeroes: true,
}, },
}, },
{ {
disp: 'tiny', disp: 'tiny',
props: { value: new BN(1), symbol: 'ETH' }, props: { value: new BN(1), symbol: 'ZEC' },
}, },
{ {
disp: 'tiny - displayShortBalance: true', disp: 'tiny - displayShortBalance: true',
props: { props: {
value: new BN(1), value: new BN(1),
symbol: 'ETH', symbol: 'ZEC',
displayShortBalance: true, displayShortBalance: true,
}, },
}, },
@ -57,7 +57,7 @@ const cases = [
disp: 'tiny - displayShortBalance: 2', disp: 'tiny - displayShortBalance: 2',
props: { props: {
value: new BN(1), value: new BN(1),
symbol: 'ETH', symbol: 'ZEC',
displayShortBalance: 2, displayShortBalance: 2,
}, },
}, },

View File

@ -34,7 +34,7 @@ const cases: Case[] = [
}, },
}, },
{ {
disp: 'ETH Address Only User', disp: 'ZEC Address Only User',
props: { props: {
user: { user: {
...user, ...user,
@ -43,7 +43,7 @@ const cases: Case[] = [
}, },
}, },
{ {
disp: 'No Avatar, No ETH Address User', disp: 'No Avatar, No ZEC Address User',
props: { props: {
user: { user: {
...user, ...user,

View File

@ -8,7 +8,7 @@ import {
import { PROPOSAL_CATEGORY } from 'api/constants'; import { PROPOSAL_CATEGORY } from 'api/constants';
import BN from 'bn.js'; import BN from 'bn.js';
const oneEth = new BN('1000000000000000000'); const oneZec = new BN('100000000');
export function getGovernanceMilestonesProps({ export function getGovernanceMilestonesProps({
isContributor = true, isContributor = true,
@ -26,7 +26,7 @@ export function getGovernanceMilestonesProps({
}; };
} }
export function getProposalWithCrowdFund({ export function generateProposal({
amount = 10, amount = 10,
funded = 5, funded = 5,
created = Date.now(), created = Date.now(),
@ -42,8 +42,9 @@ export function getProposalWithCrowdFund({
contributorOverrides?: Array<Partial<Contributor>>; contributorOverrides?: Array<Partial<Contributor>>;
milestoneCount?: number; milestoneCount?: number;
}) { }) {
const amountBn = oneEth.mul(new BN(amount)); const amountBn = oneZec.mul(new BN(amount));
const fundedBn = oneEth.mul(new BN(funded)); const fundedBn = oneZec.mul(new BN(funded));
const percentFunded = amount / funded;
let contributors = [ let contributors = [
{ {
@ -96,34 +97,27 @@ export function getProposalWithCrowdFund({
return ts.slice(rand).join(''); return ts.slice(rand).join('');
}; };
const genMilestone = (overrides: Partial<ProposalMilestone> = {}) => { const genMilestone = (overrides: Partial<ProposalMilestone> = {}): ProposalMilestone => {
const now = new Date(); const now = new Date();
if (overrides.index) { if (overrides.index) {
const estimate = new Date(now.setMonth(now.getMonth() + overrides.index)); const estimate = new Date(now.setMonth(now.getMonth() + overrides.index));
overrides.dateEstimated = estimate.toISOString(); overrides.dateEstimated = estimate.toISOString();
} }
return Object.assign( const defaults: ProposalMilestone = {
{ title: 'Milestone A',
title: 'Milestone A', content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
tempor incididunt ut labore et dolore magna aliqua.`, dateEstimated: '2018-10-01T00:00:00+00:00',
content: '', immediatePayout: true,
dateEstimated: '2018-10-01T00:00:00+00:00', index: 0,
immediatePayout: true, state: MILESTONE_STATE.WAITING,
index: 0, amount: amountBn,
state: MILESTONE_STATE.WAITING, isPaid: false,
amount: amountBn, isImmediatePayout: true,
amountAgainstPayout: new BN(0), payoutPercent: '33',
percentAgainstPayout: 0, };
payoutRequestVoteDeadline: 0, return { ...defaults, ...overrides };
isPaid: false,
isImmediatePayout: true,
payoutPercent: '33',
stage: 'NOT_REQUESTED',
},
overrides,
);
}; };
const milestones = [...Array(milestoneCount).keys()].map(i => { const milestones = [...Array(milestoneCount).keys()].map(i => {
@ -148,7 +142,12 @@ export function getProposalWithCrowdFund({
proposalId: 12345, proposalId: 12345,
proposalUrlId: '12345-crowdfund-title', proposalUrlId: '12345-crowdfund-title',
proposalAddress: '0x033fDc6C01DC2385118C7bAAB88093e22B8F0710', proposalAddress: '0x033fDc6C01DC2385118C7bAAB88093e22B8F0710',
payoutAddress: 'z123',
dateCreated: created / 1000, dateCreated: created / 1000,
deadlineDuration: 86400 * 60,
target: amountBn,
funded: fundedBn,
percentFunded,
title: 'Crowdfund Title', title: 'Crowdfund Title',
brief: 'A cool test crowdfund', brief: 'A cool test crowdfund',
content: 'body', content: 'body',

View File

@ -1,4 +1,4 @@
import { Wei } from 'utils/units'; import { Zat } from 'utils/units';
export enum MILESTONE_STATE { export enum MILESTONE_STATE {
WAITING = 'WAITING', WAITING = 'WAITING',
@ -10,10 +10,7 @@ export enum MILESTONE_STATE {
export interface Milestone { export interface Milestone {
index: number; index: number;
state: MILESTONE_STATE; state: MILESTONE_STATE;
amount: Wei; amount: Zat;
amountAgainstPayout: Wei;
percentAgainstPayout: number;
payoutRequestVoteDeadline: number;
isPaid: boolean; isPaid: boolean;
isImmediatePayout: boolean; isImmediatePayout: boolean;
} }
@ -23,7 +20,6 @@ export interface ProposalMilestone extends Milestone {
immediatePayout: boolean; immediatePayout: boolean;
dateEstimated: string; dateEstimated: string;
payoutPercent: string; payoutPercent: string;
stage: string;
title: string; title: string;
} }

View File

@ -1,12 +1,13 @@
import { Wei } from 'utils/units'; import BN from 'bn.js';
import { Zat } from 'utils/units';
import { PROPOSAL_CATEGORY } from 'api/constants'; import { PROPOSAL_CATEGORY } from 'api/constants';
import { import {
CreateMilestone, CreateMilestone,
ProposalMilestone,
Update, Update,
User, User,
Comment, Comment,
} from 'types'; } from 'types';
import { ProposalMilestone } from './milestone';
export interface TeamInvite { export interface TeamInvite {
id: number; id: number;
@ -17,7 +18,7 @@ export interface TeamInvite {
export interface Contributor { export interface Contributor {
address: string; address: string;
contributionAmount: Wei; contributionAmount: Zat;
refundVote: boolean; refundVote: boolean;
refunded: boolean; refunded: boolean;
proportionalContribution: string; proportionalContribution: string;
@ -40,18 +41,14 @@ export interface ProposalDraft {
invites: TeamInvite[]; invites: TeamInvite[];
} }
export interface Proposal {
proposalId: number; export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
proposalAddress: string; proposalAddress: string;
proposalUrlId: string; proposalUrlId: string;
dateCreated: number; target: BN;
title: string; funded: BN;
brief: string; percentFunded: number;
content: string;
stage: string;
category: PROPOSAL_CATEGORY;
milestones: ProposalMilestone[]; milestones: ProposalMilestone[];
team: User[];
} }
export interface TeamInviteWithProposal extends TeamInvite { export interface TeamInviteWithProposal extends TeamInvite {
@ -74,6 +71,6 @@ export interface UserProposal {
title: string; title: string;
brief: string; brief: string;
team: User[]; team: User[];
funded: Wei; funded: BN;
target: Wei; target: BN;
} }