From ed88d2215b6155290618aa32484a08378eeb2615 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 15:57:23 -0300 Subject: [PATCH 01/24] Update CrowdsaleBlock to be used for any tier --- src/components/stepThree/CrowdsaleBlock.js | 123 ++++++++++++++------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/src/components/stepThree/CrowdsaleBlock.js b/src/components/stepThree/CrowdsaleBlock.js index 86002e726..810d0989f 100644 --- a/src/components/stepThree/CrowdsaleBlock.js +++ b/src/components/stepThree/CrowdsaleBlock.js @@ -1,43 +1,84 @@ -import React from "react"; -import "../../assets/stylesheets/application.css"; -import { WhitelistInputBlock } from "../Common/WhitelistInputBlock"; -import { defaultCompanyEndDate } from "../../utils/utils"; -import { InputField } from "../Common/InputField"; -import { RadioInputField } from "../Common/RadioInputField"; -import { inject, observer } from "mobx-react"; -import { VALIDATION_MESSAGES, TEXT_FIELDS, DESCRIPTION } from "../../utils/constants"; -const { START_TIME, END_TIME, RATE, SUPPLY, CROWDSALE_SETUP_NAME, ALLOWMODIFYING } = TEXT_FIELDS; +import React from 'react' +import '../../assets/stylesheets/application.css' +import { WhitelistInputBlock } from '../Common/WhitelistInputBlock' +import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' +import { InputField } from '../Common/InputField' +import { RadioInputField } from '../Common/RadioInputField' +import { inject, observer } from 'mobx-react' +import { VALIDATION_TYPES, VALIDATION_MESSAGES, TEXT_FIELDS, DESCRIPTION } from '../../utils/constants' +import { BigNumberInput } from '../Common/BigNumberInput' +import update from 'immutability-helper' -@inject("tierStore") +const { START_TIME, END_TIME, RATE, SUPPLY, CROWDSALE_SETUP_NAME, ALLOWMODIFYING } = TEXT_FIELDS +const { EMPTY, INVALID } = VALIDATION_TYPES + +@inject('tierStore') @observer export class CrowdsaleBlock extends React.Component { - componentWillMount() { - const { tierStore, num } = this.props; - const startTime = tierStore.tiers[num - 1].endTime; - const endTime = defaultCompanyEndDate(tierStore.tiers[num - 1].endTime); - tierStore.setTierProperty(startTime, "startTime", num); - tierStore.setTierProperty(endTime, "endTime", num); + constructor (props) { + super(props) + + this.state = { + rate: '', + validation: { + rate: { + pristine: true, + valid: INVALID + } + } + } + } + + componentWillMount () { + const { tierStore, num } = this.props + const startTime = 0 === num ? defaultCompanyStartDate() : this.tierEndTime(num - 1) + const endTime = 0 === num ? defaultCompanyEndDate(startTime) : defaultCompanyEndDate(this.tierEndTime(num - 1)) + + tierStore.setTierProperty(startTime, 'startTime', num) + tierStore.setTierProperty(endTime, 'endTime', num) } - updateTierStore = (event, property) => { - const { tierStore, num } = this.props; - const value = event.target.value; - tierStore.setTierProperty(value, property, num); - tierStore.validateTiers(property, num); - }; + tierEndTime = (index) => this.props.tierStore.tiers[index].endTime + + updateTierStore = (value, property) => { + const { num, tierStore } = this.props + + tierStore.setTierProperty(value, property, num) + tierStore.validateTiers(property, num) + } - render() { - let { num, tierStore } = this.props; - let whitelistInputBlock = ( + updateRate = ({ value, pristine, valid }) => { + const { num, tierStore } = this.props + + const newState = update(this.state, { + validation: { + rate: { + $set: { + pristine, + valid + } + } + } + }) + newState.rate = value + + this.setState(newState) + tierStore.updateRate(value, valid, num) + } + + render () { + const { num, tierStore } = this.props + const whitelistInputBlock = (

Whitelist

- +
- ); + ) + return ( -
+
this.updateTierStore(e, "tier")} + onChange={e => this.updateTierStore(e.target.value, 'tier')} description={DESCRIPTION.CROWDSALE_SETUP_NAME} /> this.updateTierStore(e, "updatable")} + onChange={e => this.updateTierStore(e.target.value, 'updatable')} description={DESCRIPTION.ALLOW_MODIFYING} />
@@ -67,7 +108,7 @@ export class CrowdsaleBlock extends React.Component { value={tierStore.tiers[num].startTime} valid={tierStore.validTiers[num].startTime} errorMessage={VALIDATION_MESSAGES.MULTIPLE_TIERS_START_TIME} - onChange={e => this.updateTierStore(e, "startTime")} + onChange={e => this.updateTierStore(e.target.value, 'startTime')} description={DESCRIPTION.START_TIME} /> this.updateTierStore(e, "endTime")} + onChange={e => this.updateTierStore(e.target.value, 'endTime')} description={DESCRIPTION.END_TIME} />
- this.updateTierStore(e, "rate")} + onChange={this.updateRate} description={DESCRIPTION.RATE} /> this.updateTierStore(e, "supply")} + onChange={e => this.updateTierStore(e.target.value, 'supply')} description={DESCRIPTION.SUPPLY} />
- {tierStore.tiers[0].whitelistEnabled === "yes" ? whitelistInputBlock : ""} + {tierStore.tiers[0].whitelistEnabled === 'yes' ? whitelistInputBlock : ''}
- ); + ) } } From 78702646432d6c3c0c518aba3f787e67f40487e4 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 15:57:38 -0300 Subject: [PATCH 02/24] Add CrowdsaleBlock tests --- .../stepThree/CrowdsaleBlock.spec.js | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/components/stepThree/CrowdsaleBlock.spec.js diff --git a/src/components/stepThree/CrowdsaleBlock.spec.js b/src/components/stepThree/CrowdsaleBlock.spec.js new file mode 100644 index 000000000..e5892e4d5 --- /dev/null +++ b/src/components/stepThree/CrowdsaleBlock.spec.js @@ -0,0 +1,148 @@ +import React from 'react' +import TierStore from '../../stores/TierStore' +import { CrowdsaleBlock } from './CrowdsaleBlock' +import MockDate from 'mockdate' +import moment from 'moment' +import { VALIDATION_TYPES } from '../../utils/constants' +import { Provider } from 'mobx-react' +import Adapter from 'enzyme-adapter-react-15' +import toJson from 'enzyme-to-json' +import { configure, mount } from 'enzyme' +import { defaultTier, defaultTierValidations } from '../../utils/constants' + +configure({ adapter: new Adapter() }) + +const currentTime = '2018-03-05T11:00:00' +const { INVALID } = VALIDATION_TYPES + +MockDate.set(currentTime) + +describe('CrowdsaleBlock', () => { + const INPUT_EVENT = { + CHANGE: 'change', + CLICK: 'click' + } + + const addCrowdsale = (num) => { + const newTier = Object.assign({}, defaultTier) + const newTierValidations = Object.assign({}, defaultTierValidations) + + newTier.tier = `Tier ${num + 1}` + + if (0 === num) { + newTier.whitelistEnabled = 'no' + newTier.walletAddress = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' + } + + tierStore.addTier(newTier) + tierStore.addTierValidations(newTierValidations) + } + + let changeMock + let tierStore + let initialTierWrapper + + beforeEach(() => { + tierStore = new TierStore() + + addCrowdsale(0) + + changeMock = { target: { value: '' } } + + initialTierWrapper = mount() + }) + + it('Should render the component for the first Tier', () => { + expect(toJson(initialTierWrapper)).toMatchSnapshot() + }) + + it('Should render the component for the second Tier', () => { + addCrowdsale(1) + const wrapper = mount() + + expect(toJson(wrapper)).toMatchSnapshot() + }) + + it('Should render the component for the second Tier with whitelist enabled', () => { + addCrowdsale(1) + tierStore.setTierProperty('yes', 'whitelistEnabled', 0) + const wrapper = mount() + + expect(toJson(wrapper)).toMatchSnapshot() + }) + + it('Should set current time + 5 minutes in startTime (first tier)', () => { + const expectedStartTime = moment(currentTime).add(5, 'minutes') + const startTimeValue = initialTierWrapper.find('input[type="datetime-local"]').at(0).props().value + + expect(expectedStartTime.isSame(startTimeValue)).toBeTruthy() + }) + + it('Should set endTime at the beginning of 4 days in the future of startTime (first tier)', () => { + const expectedEndTime = moment(currentTime).add(4, 'days').startOf('day') + const endTimeValue = initialTierWrapper.find('input[type="datetime-local"]').at(1).props().value + + expect(expectedEndTime.isSame(endTimeValue)).toBeTruthy() + }) + + it('Should set startTime at the same time as the end time of the previous tier (second tier)', () => { + addCrowdsale(1) + const secondTierWrapper = mount() + const firstTierEndTimeValue = initialTierWrapper.find('input[type="datetime-local"]').at(1).props().value + const secondTierStartTimeValue = secondTierWrapper.find('input[type="datetime-local"]').at(0).props().value + + expect(firstTierEndTimeValue).toBe(secondTierStartTimeValue) + }) + + it('Should give error if startTime of the second tier is previous to the endTime of the first tier', () => { + addCrowdsale(1) + const secondTierWrapper = mount() + const firstTierEndTimeValue = initialTierWrapper.find('input[type="datetime-local"]').at(1).props().value + const secondTierStartTime = secondTierWrapper.find('input[type="datetime-local"]').at(0) + + changeMock.target.value = moment(firstTierEndTimeValue).subtract(1, 'days').toJSON() + secondTierStartTime.simulate(INPUT_EVENT.CHANGE, changeMock) + + const secondTierStartTimeProps = secondTierWrapper.find('InputField[title="Start Time"]').props() + + expect(moment(firstTierEndTimeValue).subtract(1, 'days').isSame(secondTierStartTimeProps.value)).toBeTruthy() + expect(secondTierStartTimeProps.valid).toBe(INVALID) + }) + + it('Should properly apply Rate update', () => { + const rate = initialTierWrapper.find('input[type="text"]').at(1) + + changeMock.target.value = '1234' + rate.simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(initialTierWrapper.find('BigNumberInput').props().value).toBe(changeMock.target.value) + }) + + it('Should properly update supply value', () => { + const supply = initialTierWrapper.find('input[type="number"]').at(0) + + changeMock.target.value = '1234' + supply.simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(initialTierWrapper.find('InputField[title="Supply"]').props().value).toBe(changeMock.target.value) + }) + + it('Should properly change End Time', () => { + const endTime = initialTierWrapper.find('input[type="datetime-local"]').at(1) + const modifiedDate = moment(endTime).subtract(1, 'days') + + changeMock.target.value = modifiedDate.toJSON() + endTime.simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(modifiedDate.isSame(initialTierWrapper.find('InputField[title="End Time"]').props().value)).toBeTruthy() + }) + + it('Should properly change Tier name', () => { + const tierName = initialTierWrapper.find('input[type="text"]').at(0) + + changeMock.target.value = 'The first Tier' + tierName.simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(initialTierWrapper.find('InputField[title="Crowdsale setup name"]').props().value).toBe(changeMock.target.value) + }) +}) From e0a7239ff756901f190ea1d6e5962b73a8ffcbf5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 15:58:51 -0300 Subject: [PATCH 03/24] Update StepThree to use CrowdsaleBlock for all tiers --- src/components/stepThree/index.js | 318 +++++++++++------------------- 1 file changed, 112 insertions(+), 206 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index 13c525a9b..8f5597980 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -2,13 +2,11 @@ import React from "react"; import "../../assets/stylesheets/application.css"; import { Link } from "react-router-dom"; import { setExistingContractParams, getNetworkVersion, getNetWorkNameById } from "../../utils/blockchainHelpers"; -import { defaultCompanyStartDate } from "./utils"; -import { defaultCompanyEndDate, gweiToWei, weiToGwei } from "../../utils/utils"; +import { gweiToWei, weiToGwei } from "../../utils/utils"; import { StepNavigation } from "../Common/StepNavigation"; import { InputField } from "../Common/InputField"; import { RadioInputField } from "../Common/RadioInputField"; import { CrowdsaleBlock } from "./CrowdsaleBlock"; -import { WhitelistInputBlock } from "../Common/WhitelistInputBlock"; import { NAVIGATION_STEPS, VALIDATION_MESSAGES, @@ -16,8 +14,9 @@ import { TEXT_FIELDS, CONTRACT_TYPES, CHAINS, - DESCRIPTION -} from "../../utils/constants"; + defaultTier, + defaultTierValidations +} from '../../utils/constants' import { inject, observer } from "mobx-react"; import { Loader } from '../Common/Loader' import { noGasPriceAvailable, warningOnMainnetAlert } from '../../utils/alerts' @@ -25,34 +24,33 @@ import { NumericInput } from '../Common/NumericInput' import update from 'immutability-helper' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; -const { EMPTY, VALID, INVALID } = VALIDATION_TYPES; -const { - START_TIME, - END_TIME, - MINCAP, - RATE, - SUPPLY, - WALLET_ADDRESS, - CROWDSALE_SETUP_NAME, - ALLOWMODIFYING, - ENABLE_WHITELISTING -} = TEXT_FIELDS; - -@inject("contractStore", "crowdsaleBlockListStore", "pricingStrategyStore", "web3Store", "tierStore", "generalStore", "gasPriceStore", "reservedTokenStore", "deploymentStore", "tokenStore") +const { VALID, INVALID } = VALIDATION_TYPES; +const { MINCAP, WALLET_ADDRESS, ENABLE_WHITELISTING } = TEXT_FIELDS; + +@inject( + "contractStore", + "crowdsaleBlockListStore", + "web3Store", + "tierStore", + "generalStore", + "gasPriceStore", + "reservedTokenStore", + "deploymentStore", + "tokenStore" +) @observer export class stepThree extends React.Component { constructor(props) { super(props); - const { contractStore, crowdsaleBlockListStore, tierStore, gasPriceStore } = props; - window.scrollTo(0, 0); + + const { contractStore, crowdsaleBlockListStore, gasPriceStore } = props; + if (contractStore.crowdsale.addr.length > 0) { contractStore.setContractProperty("pricingStrategy", "addr", []); setExistingContractParams(contractStore.abi, contractStore.addr[0], contractStore.setContractProperty); } - crowdsaleBlockListStore.emptyList(); - tierStore.setTierProperty("Tier 1", "tier", 0); - tierStore.setTierProperty("off", "updatable", 0); - tierStore.setTierProperty("no", "whitelistEnabled", 0); + + crowdsaleBlockListStore.emptyList() this.state = { loading: true, @@ -71,80 +69,75 @@ export class stepThree extends React.Component { } } - showErrorMessages = parent => { - this.props.tierStore.invalidateToken(); - }; + componentDidMount () { + const { gasPriceStore } = this.props - addCrowdsale() { - const { crowdsaleBlockListStore, tierStore } = this.props; - let num = crowdsaleBlockListStore.blockList.length + 1; - const newTier = { - tier: "Tier " + (num + 1), - supply: 0, - rate: 0, - updatable: "off", - whitelist: [], - whitelistElements: [] - }; - - const newTierValidations = { - tier: VALID, - startTime: VALID, - endTime: VALID, - supply: EMPTY, - rate: EMPTY - }; - - tierStore.addTier(newTier); - tierStore.addTierValidations(newTierValidations); - this.addCrowdsaleBlock(num); + gasPriceStore.updateValues() + .then(() => this.setGasPrice(gasPriceStore.slow)) + .catch(() => noGasPriceAvailable()) + .then(() => { + this.addCrowdsale() + this.setState({ loading: false }) + window.scrollTo(0, 0) + }) + } + + showErrorMessages = () => { + const { tierStore } = this.props + + tierStore.invalidateToken() } updateTierStore = (event, property, index) => { - const { tierStore } = this.props; - const value = event.target.value; - tierStore.setTierProperty(value, property, index); - tierStore.validateTiers(property, index); - }; + const { tierStore } = this.props + const value = event.target.value - goToDeploymentStage = () => { - this.props.history.push('/4') + tierStore.setTierProperty(value, property, index) + tierStore.validateTiers(property, index) } - addCrowdsaleBlock(num) { - this.props.crowdsaleBlockListStore.addCrowdsaleItem(); + addCrowdsale() { + const { crowdsaleBlockListStore, tierStore, web3Store } = this.props + const { curAddress } = web3Store + + const num = crowdsaleBlockListStore.blockList.length + const newTier = Object.assign({}, defaultTier) + const newTierValidations = Object.assign({}, defaultTierValidations) + + newTier.tier = `Tier ${num + 1}` + + if (num === 0) { + newTier.whitelistEnabled = "no" + newTier.walletAddress = curAddress + } + + tierStore.addTier(newTier) + tierStore.addTierValidations(newTierValidations) + this.addCrowdsaleBlock(num) } - renderLink() { - return ( -
-
this.addCrowdsale()} className="button button_fill_secondary"> - {" "} - Add Tier -
- this.beforeNavigate(e)} - className="button button_fill" - > - Continue - -
- ); + addCrowdsaleBlock (num) { + const { crowdsaleBlockListStore } = this.props + + crowdsaleBlockListStore.addCrowdsaleItem() + } + + goToDeploymentStage = () => { + this.props.history.push('/4') } beforeNavigate = e => { - e.preventDefault(); - e.stopPropagation(); + e.preventDefault() + e.stopPropagation() - const { tierStore, gasPriceStore } = this.props; + const { tierStore, gasPriceStore } = this.props const gasPriceIsValid = gasPriceStore.custom.id !== this.state.gasPriceSelected || this.state.validation.gasPrice.valid === VALID console.log('gasPriceIsValid', gasPriceIsValid) for (let index = 0; index < tierStore.tiers.length; index++) { - tierStore.validateTiers("endTime", index); - tierStore.validateTiers("startTime", index); + tierStore.validateTiers('endTime', index) + tierStore.validateTiers('startTime', index) } if (tierStore.areTiersValid && gasPriceIsValid) { @@ -179,21 +172,25 @@ export class stepThree extends React.Component { this.showErrorMessages(e) }) } else { - this.showErrorMessages(e); + this.showErrorMessages(e) } - }; + } - componentDidMount() { - const { tierStore, web3Store, gasPriceStore } = this.props; - const { curAddress } = web3Store; - tierStore.setTierProperty(curAddress, "walletAddress", 0); - tierStore.setTierProperty(defaultCompanyStartDate(), "startTime", 0); - tierStore.setTierProperty(defaultCompanyEndDate(tierStore.tiers[0].startTime), "endTime", 0); + updateMinCap = ({ value, pristine, valid }) => { + const newState = update(this.state, { + validation: { + minCap: { + $set: { + pristine: pristine, + valid: valid + } + } + } + }) + newState.minCap = value - gasPriceStore.updateValues() - .then(() => this.setGasPrice(gasPriceStore.slow)) - .catch(() => noGasPriceAvailable()) - .then(() => this.setState({ loading: false })) + this.setState(newState) + this.props.tierStore.setGlobalMinCap(value) } setGasPrice({ id, price }) { @@ -223,23 +220,6 @@ export class stepThree extends React.Component { this.props.generalStore.setGasPrice(gweiToWei(value)) } - updateMinCap = ({ value, pristine, valid }) => { - const newState = update(this.state, { - validation: { - minCap: { - $set: { - pristine: pristine, - valid: valid - } - } - } - }) - newState.minCap = value - - this.setState(newState) - this.props.tierStore.setGlobalMinCap(value) - } - renderGasPriceInput() { const { generalStore, gasPriceStore } = this.props @@ -323,7 +303,8 @@ export class stepThree extends React.Component { render() { const { contractStore, crowdsaleBlockListStore, tierStore } = this.props; - let globalSettingsBlock = ( + + const globalSettingsBlock = (

Global settings

@@ -333,11 +314,12 @@ export class stepThree extends React.Component { side="left" type="text" title={WALLET_ADDRESS} - value={tierStore.tiers[0].walletAddress} + value={tierStore.tiers[0] && tierStore.tiers[0].walletAddress} valid={tierStore.validTiers[0] && tierStore.validTiers[0].walletAddress} errorMessage={VALIDATION_MESSAGES.WALLET_ADDRESS} onChange={e => this.updateTierStore(e, "walletAddress", 0)} - description={`Where the money goes after investors transactions. Immediately after each transaction. We recommend to setup a multisig wallet with hardware based signers.`} + description="Where the money goes after investors transactions. Immediately after each transaction. We + recommend to setup a multisig wallet with hardware based signers." /> {this.renderGasPriceInput()}
@@ -345,8 +327,9 @@ export class stepThree extends React.Component { this.updateWhitelistEnabled(e)} - description={`Enables whitelisting. If disabled, anyone can participate in the crowdsale.`} + description="Enables whitelisting. If disabled, anyone can participate in the crowdsale." />
- ); + ) + if (contractStore.contractType === CONTRACT_TYPES.whitelistwithcap) { - let whitelistInputBlock = ( -
-
-

Whitelist

-
- -
- ); return (
- +
-
+

Crowdsale setup

-

- The most important and exciting part of the crowdsale process. Here you can define parameters of your - crowdsale campaign. -

+

The most important and exciting part of the crowdsale process. Here you can + define parameters of your crowdsale campaign.

{globalSettingsBlock}
- {/* First tier */} -
-
-
- this.updateTierStore(e, "tier", 0)} - description={DESCRIPTION.CROWDSALE_SETUP_NAME} - /> - this.updateTierStore(e, "updatable", 0)} - description={DESCRIPTION.ALLOW_MODIFYING} - /> -
-
- this.updateTierStore(e, "startTime", 0)} - description={DESCRIPTION.START_TIME} - /> - this.updateTierStore(e, "endTime", 0)} - description={DESCRIPTION.END_TIME} - /> -
-
- this.updateTierStore(e, "rate", 0)} - description={DESCRIPTION.RATE} - /> - this.updateTierStore(e, "supply", 0)} - description={DESCRIPTION.SUPPLY} - /> -
-
- {tierStore.tiers[0].whitelistEnabled === "yes" ? whitelistInputBlock : ""} -
- - {/* Other tiers */}
{crowdsaleBlockListStore.blockList}
-
{this.renderLink()}
+
+
this.addCrowdsale()} className="button button_fill_secondary">Add Tier
+ this.beforeNavigate(e)} className="button button_fill" to="/4">Continue +
+
- ); + ) } } } From e6815243a392b73511ef7436c601d67070060a85 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:01:07 -0300 Subject: [PATCH 04/24] Update TierStore (remove default tiers/validTiers & create method to update already validated rate) --- src/stores/TierStore.js | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/stores/TierStore.js b/src/stores/TierStore.js index 1d652c59f..fe4b972d0 100644 --- a/src/stores/TierStore.js +++ b/src/stores/TierStore.js @@ -1,17 +1,21 @@ import { observable, action, computed } from 'mobx'; -import { VALIDATION_TYPES, defaultTiers } from '../utils/constants' +import { VALIDATION_TYPES } from '../utils/constants' import { - validateName, validateTime, validateSupply, validateRate, validateAddress, validateLaterTime, - validateLaterOrEqualTime, validateTier, validateMinCap + validateTime, + validateSupply, + validateAddress, + validateLaterTime, + validateLaterOrEqualTime, + validateTier } from '../utils/utils' import autosave from './autosave' const { VALID, INVALID } = VALIDATION_TYPES class TierStore { - @observable tiers; - @observable validTiers; - @observable globalMinCap = ''; + @observable tiers + @observable validTiers + @observable globalMinCap = '' constructor() { this.reset() @@ -19,16 +23,8 @@ class TierStore { } @action reset = () => { - this.tiers = defaultTiers.slice() - this.validTiers = [{ - name: 'VALIDATED', - walletAddress: 'VALIDATED', - rate: 'EMPTY', - supply: 'EMPTY', - startTime: 'VALIDATED', - endTime: 'VALIDATED', - updatable: "VALIDATED" - }] + this.tiers = [] + this.validTiers = [] } @action setGlobalMinCap = (minCap) => { @@ -63,9 +59,6 @@ class TierStore { @action validateTiers = (property, index) => { switch (property){ - case 'name': - this.validTiers[index][property] = validateName(this.tiers[index][property]) ? VALID : INVALID - break case 'tier': this.validTiers[index][property] = validateTier(this.tiers[index][property]) ? VALID : INVALID break @@ -75,9 +68,6 @@ class TierStore { case 'supply': this.validTiers[index][property] = validateSupply(this.tiers[index][property]) ? VALID : INVALID break - case 'rate': - this.validTiers[index][property] = validateRate(this.tiers[index][property]) ? VALID : INVALID - break case 'startTime': if (index > 0) { this.validTiers[index][property] = validateLaterOrEqualTime(this.tiers[index][property], this.tiers[index - 1].endTime) ? VALID : INVALID @@ -93,6 +83,11 @@ class TierStore { } } + @action updateRate = (value, validity, tierIndex) => { + this.tiers[tierIndex].rate = value + this.validTiers[tierIndex].rate = validity + } + @action validateEditedTier = (property, index) => { switch (property) { case 'endTime': From 7ba623ca20d65d042f2cce9d8374c351d3e97b1d Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:01:20 -0300 Subject: [PATCH 05/24] Create BigNumberInput --- src/components/Common/BigNumberInput.js | 125 ++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/components/Common/BigNumberInput.js diff --git a/src/components/Common/BigNumberInput.js b/src/components/Common/BigNumberInput.js new file mode 100644 index 000000000..922c4afcc --- /dev/null +++ b/src/components/Common/BigNumberInput.js @@ -0,0 +1,125 @@ +import React, { Component } from 'react' +import { BigNumber } from 'bignumber.js' +import { VALIDATION_TYPES } from '../../utils/constants' +import { InputField } from './InputField' + +const { VALID, INVALID } = VALIDATION_TYPES + +export class BigNumberInput extends Component { + constructor (props) { + super(props) + + this.state = { + value: props.value || '', + pristine: props.pristine !== undefined ? props.pristine : true, + valid: props.valid || VALID + } + } + + componentWillReceiveProps (newProps) { + const { value, pristine, valid } = newProps + + this.setState({ value, pristine, valid }) + } + + componentDidUpdate (prevProps) { + const { acceptEmpty, acceptFloat, minDecimals, maxDecimals, min, max } = prevProps + + if ( + prevProps.acceptEmpty !== acceptEmpty || + prevProps.acceptFloat !== acceptFloat || + prevProps.minDecimals !== minDecimals || + prevProps.maxDecimals !== maxDecimals || + prevProps.min !== min || + prevProps.max !== max + ) { + // re-check validity if any of the props had changed + this.validate(this.state.value) + } + } + + validate = (value) => { + const { acceptEmpty, acceptFloat, minDecimals, maxDecimals, min, max } = this.props + const newState = { + pristine: false + } + + if (isNaN(Number(value)) || isNaN(parseFloat(value))) { + newState.value = '' + newState.valid = acceptEmpty ? VALID : INVALID + + } else { + const number = new BigNumber(value) + const decimals = number.decimalPlaces() + let isValid = true + + if (acceptFloat) { + if (maxDecimals !== undefined) { + isValid = decimals <= maxDecimals + } + + if (isValid && minDecimals !== undefined) { + isValid = decimals >= minDecimals + } + + } else { + isValid = !decimals + } + + if (isValid && min !== undefined) { + isValid = number.gte(min) + } + + if (isValid && max !== undefined) { + isValid = number.lte(max) + } + + newState.value = number.toFixed() + newState.valid = isValid ? VALID : INVALID + } + + this.setState(newState) + this.props.onChange(newState) + } + + onKeyPress = e => { + const { acceptFloat, min, max } = this.props + const { key } = e + const isValidNumericKey = /[0-9.+e-]/ + const isValidIntegerKey = /[0-9-]/ + + if (!isValidNumericKey.test(key)) e.preventDefault() + if (!acceptFloat && !isValidIntegerKey.test(key)) e.preventDefault() + if (!acceptFloat && key === '-' && min >= 0 && max >= 0) e.preventDefault() + } + + onPaste = e => { + if (isNaN(Number(e.clipboardData.getData('text/plain')))) e.preventDefault() + } + + onChange = e => { + this.validate(e.target.value) + } + + render () { + const { value, pristine, valid } = this.state + const { disabled, side, errorMessage, title, description } = this.props + + return ( + + ) + } +} From c9b8c55f544ab6a9c0b0363e26b4a0df6c4fb53c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:01:33 -0300 Subject: [PATCH 06/24] Add test for BigNumberInput --- src/components/Common/BigNumberInput.spec.js | 370 +++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 src/components/Common/BigNumberInput.spec.js diff --git a/src/components/Common/BigNumberInput.spec.js b/src/components/Common/BigNumberInput.spec.js new file mode 100644 index 000000000..7b1b8d747 --- /dev/null +++ b/src/components/Common/BigNumberInput.spec.js @@ -0,0 +1,370 @@ +import React from 'react' +import { BigNumberInput } from './BigNumberInput' +import { BigNumber } from 'bignumber.js' +import { DESCRIPTION, TEXT_FIELDS, VALIDATION_MESSAGES, VALIDATION_TYPES } from '../../utils/constants' +import renderer from 'react-test-renderer' +import Adapter from 'enzyme-adapter-react-15' +import { configure, mount } from 'enzyme' + +configure({ adapter: new Adapter() }) + +describe('BigNumberInput', () => { + const { INVALID, VALID } = VALIDATION_TYPES + + const INPUT_EVENT = { + KEYPRESS: 'keypress', + CHANGE: 'change', + PASTE: 'paste' + } + + let changeMock + let keypressMock + let pasteMock + let bigNumberInputComponent + let wrapperMemo, wrapper + let inputMemo, input + + beforeEach(() => { + changeMock = { target: { value: '' } } + + keypressMock = { key: '1', preventDefault: jest.fn() } + + pasteMock = { + preventDefault: jest.fn(), + clipboardData: { + getData: () => 'e123e123' + } + } + + bigNumberInputComponent = { + side: 'left', + title: TEXT_FIELDS.RATE, + description: DESCRIPTION.RATE, + errorMessage: VALIDATION_MESSAGES.RATE, + onChange: jest.fn() + } + + wrapperMemo = undefined + wrapper = () => wrapperMemo || (wrapperMemo = mount(React.createElement(BigNumberInput, bigNumberInputComponent))) + + inputMemo = undefined + input = () => inputMemo || (inputMemo = wrapper().find('input').at(0)) + }) + + it('Should render the component', () => { + bigNumberInputComponent.min = 1 + bigNumberInputComponent.max = 1e18 + bigNumberInputComponent.acceptEmpty = false + bigNumberInputComponent.acceptFloat = true + bigNumberInputComponent.minDecimals = 0 + bigNumberInputComponent.maxDecimals = 4 + bigNumberInputComponent.pristine = true + + const reactElement = React.createElement(BigNumberInput, bigNumberInputComponent) + const element = renderer.create(reactElement) + + expect(element.toJSON()).toMatchSnapshot() + }) + + describe('paste event', () => { + [ + { value: '123', expected: 0 }, + { value: '0', expected: 0 }, + { value: '1e10', expected: 0 }, + { value: '1e+10', expected: 0 }, + { value: '1e-10', expected: 0 }, + { value: '1.23', expected: 0 }, + { value: '.123', expected: 0 }, + { value: '-123', expected: 0 }, + { value: '123e', expected: 1 }, + { value: '123e123123e12', expected: 1 }, + { value: '12345678901234567890abcd123', expected: 1 }, + { value: '123abc123', expected: 1 }, + ].forEach(testCase => { + const action = testCase.expected ? 'fail' : 'pass' + + it(`Should ${action} for '${testCase.value}'`, () => { + pasteMock.clipboardData.getData = () => testCase.value + input().simulate(INPUT_EVENT.PASTE, pasteMock) + + expect(pasteMock.preventDefault).toHaveBeenCalledTimes(testCase.expected) + }) + }) + }) + + describe('key press event', () => { + [ + { + value: '+', + expected: 1, + props: { min: 0, max: 1e18 } + }, + { + value: '.', + expected: 1, + props: { min: 0, max: 1e18 } + }, + { + value: 'e', + expected: 1, + props: { min: 0, max: 1e18 } + }, + { + value: '-', + expected: 1, + props: { min: 0, max: 1e18 } + }, + { + value: '1', + expected: 0, + props: { min: 0, max: 1e18 } + }, + { + value: '-', + expected: 0, + props: { min: -10, max: 1e18 } + }, + { + value: '-', + expected: 0, + props: { max: -5 } + }, + { + value: '1', + expected: 0, + props: { min: 0, max: 1e18 } + }, + { + value: '.', + expected: 0, + props: { acceptFloat: true } + }, + { + value: '-', + expected: 0, + props: { acceptFloat: true } + }, + { + value: '+', + expected: 0, + props: { acceptFloat: true } + }, + { + value: 'e', + expected: 0, + props: { acceptFloat: true } + }, + { + value: 'j', + expected: 1, + props: { acceptFloat: true } + }, + ].forEach(testCase => { + const action = testCase.expected ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.acceptFloat = testCase.props.acceptFloat + bigNumberInputComponent.minDecimals = testCase.props.minDecimals + bigNumberInputComponent.maxDecimals = testCase.props.maxDecimals + bigNumberInputComponent.min = testCase.props.min + bigNumberInputComponent.max = testCase.props.max + + keypressMock.key = testCase.value + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(testCase.expected) + }) + }) + }) + + describe('change event', () => { + describe('special characters', () => { + [ + { value: 'abc', expected: INVALID }, + { value: '#@', expected: INVALID }, + { value: '~', expected: INVALID }, + { value: 'e', expected: INVALID }, + { value: '123e', expected: INVALID }, + { value: '', expected: VALID }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.acceptEmpty = testCase.expected === VALID + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: '', + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + + describe('with float', () => { + [ + { value: '123', expected: VALID }, + { value: '123e3', expected: VALID }, + { value: '123e-3', expected: VALID }, + { value: '.123', expected: VALID }, + { value: '0.123', expected: VALID }, + { value: '1.123', expected: VALID }, + { value: '1.12e12', expected: VALID }, + { value: '1e-18', expected: VALID }, + { value: '1e-19', expected: INVALID }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.acceptFloat = true + bigNumberInputComponent.min = 1e-18 + + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: new BigNumber(testCase.value).toFixed(), + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + + describe('without float', () => { + [ + { value: '123', expected: VALID }, + { value: '123e-3', expected: INVALID }, + { value: '.123', expected: INVALID }, + { value: '0.123', expected: INVALID }, + { value: '1.123', expected: INVALID }, + { value: '10000000000000000000', expected: INVALID }, + { value: '1000000000000000000', expected: VALID }, + { value: '1000000000000000001', expected: INVALID }, + { value: '999999999999999999', expected: VALID }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.max = 1e18 + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: new BigNumber(testCase.value).toFixed(), + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + + describe('with negative', () => { + [ + { value: '-123', expected: VALID }, + { value: '-123e3', expected: INVALID }, + { value: '-.123', expected: VALID }, + { value: '-0.123', expected: VALID }, + { value: '-1.123', expected: VALID }, + { value: '-10000000000000000000', expected: INVALID }, + { value: '-99999', expected: VALID }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.acceptFloat = true + bigNumberInputComponent.min = -1e5 + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: new BigNumber(testCase.value).toFixed(), + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + + describe('without negative', () => { + [ + { value: '-1', expected: INVALID }, + { value: '0', expected: VALID }, + { value: '15', expected: VALID }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.min = 0 + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: new BigNumber(testCase.value).toFixed(), + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + + describe('max and min decimals', () => { + [ + { + value: '1e-5', + expected: VALID, + props: { minDecimals: 0, maxDecimals: 10} + }, + { + value: '1e-5', + expected: INVALID, + props: { minDecimals: 10, maxDecimals: 10} + }, + { + value: '0.0000157', + expected: VALID, + props: { minDecimals: 0, maxDecimals: 10} + }, + { + value: '0.00000000000000001544', + expected: INVALID, + props: { minDecimals: 0, maxDecimals: 10} + }, + { + value: '.6546465464654654', + expected: VALID, + props: { minDecimals: 15, maxDecimals: 16} + }, + { + value: '.654657674646546778', + expected: INVALID, + props: { minDecimals: 15, maxDecimals: 16} + }, + ].forEach(testCase => { + const action = testCase.expected === VALID ? 'pass' : 'fail' + + it(`Should ${action} for '${testCase.value}'`, () => { + bigNumberInputComponent.acceptFloat = true + bigNumberInputComponent.maxDecimals = testCase.props.maxDecimals + bigNumberInputComponent.minDecimals = testCase.props.minDecimals + + changeMock.target.value = testCase.value + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledTimes(1) + expect(bigNumberInputComponent.onChange).toHaveBeenCalledWith({ + value: new BigNumber(testCase.value).toFixed(), + pristine: false, + valid: testCase.expected + }) + }) + }) + }) + }) +}) From bde47cc3b60adc8855a203db7b5b94e44c7bede4 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:02:20 -0300 Subject: [PATCH 07/24] Update constants values for tiers/validTiers --- src/utils/constants.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index 6dbea02d5..0e35038f8 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,11 +1,31 @@ -export const defaultTiers = [{ +export const VALIDATION_TYPES = { + VALID: "VALIDATED", + EMPTY: 'EMPTY', + INVALID: 'INVALID' +} +const { VALID, EMPTY } = VALIDATION_TYPES + +export const defaultTier = { + tier: '', + rate: '', + supply: '', startTime: '', endTime: '', - walletAddress: '', - supply: '', + updatable: 'off', whitelist: [], whitelistElements: [] -}] +} + +export const defaultTierValidations = { + tier: VALID, + rate: EMPTY, + supply: EMPTY, + startTime: VALID, + endTime: VALID, + updatable: VALID +} + +export const defaultTiers = [defaultTier] export const CONTRACT_TYPES = { standard: "standard", @@ -133,13 +153,6 @@ export const TEXT_FIELDS = { GAS_PRICE: 'Gas Price' } -export const VALIDATION_TYPES = { - VALID: "VALIDATED", - EMPTY: 'EMPTY', - INVALID: 'INVALID' -} -const { VALID, EMPTY } = VALIDATION_TYPES - export const intitialStepTwoValidations = { validations: { name: EMPTY, From 9a4e0b6c51d3a7557886f0c9b7251f04fca0eaf5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:03:28 -0300 Subject: [PATCH 08/24] Update manage to use 'tier' attribute --- src/components/manage/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/manage/utils.js b/src/components/manage/utils.js index 5fff889dd..c084ae54e 100644 --- a/src/components/manage/utils.js +++ b/src/components/manage/utils.js @@ -228,7 +228,7 @@ export const processTier = (crowdsaleAddress, crowdsaleNum) => { newTier.startTime = formatDate(startsAt) newTier.endTime = formatDate(endsAt) newTier.updatable = updatable - newTier.name = name + newTier.tier = name initialValues.updatable = newTier.updatable initialValues.index = crowdsaleNum From 21aa27e3ef69f052fd40b44730bd21b491e26b19 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:06:20 -0300 Subject: [PATCH 09/24] Remove unused stores --- src/components/stepTwo/index.js | 2 +- src/stores/StepThreeValidationStore.js | 30 -------------------------- src/stores/TierCrowdsaleListStore.js | 29 ------------------------- src/stores/index.js | 6 ------ 4 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 src/stores/StepThreeValidationStore.js delete mode 100644 src/stores/TierCrowdsaleListStore.js diff --git a/src/components/stepTwo/index.js b/src/components/stepTwo/index.js index 25def2124..3211b2a18 100644 --- a/src/components/stepTwo/index.js +++ b/src/components/stepTwo/index.js @@ -20,7 +20,7 @@ const { TOKEN_SETUP } = NAVIGATION_STEPS const { NAME, TICKER, DECIMALS } = TEXT_FIELDS const { VALID, INVALID } = VALIDATION_TYPES -@inject('tokenStore', 'web3Store', 'tierCrowdsaleListStore', 'reservedTokenStore') +@inject('tokenStore', 'web3Store', 'reservedTokenStore') @observer export class stepTwo extends Component { constructor(props) { diff --git a/src/stores/StepThreeValidationStore.js b/src/stores/StepThreeValidationStore.js deleted file mode 100644 index 28ab5c082..000000000 --- a/src/stores/StepThreeValidationStore.js +++ /dev/null @@ -1,30 +0,0 @@ -import { observable, action } from 'mobx'; -import autosave from './autosave' - -class StepThreeValidationStore { - - @observable validationsList - - constructor() { - this[0] = {} - this[0].name = 'EMPTY' - this[0].walletAddress = 'EMPTY' - this[0].rate = 'EMPTY' - this[0].supply = 'EMPTY' - this[0].startTime = 'VALIDATED' - this[0].endTime = 'VALIDATED' - this[0].updatable = "VALIDATED" - - autosave(this, 'StepThreeValidationStore') - } - - @action changeProperty = (index, property, value) => { - this[index][property] = value - } - - @action addValidationItem = (item) => { - this.validationsList.push(item) - } -} - -export default StepThreeValidationStore; diff --git a/src/stores/TierCrowdsaleListStore.js b/src/stores/TierCrowdsaleListStore.js deleted file mode 100644 index 239ddbba2..000000000 --- a/src/stores/TierCrowdsaleListStore.js +++ /dev/null @@ -1,29 +0,0 @@ -import { observable, action } from 'mobx'; -import autosave from './autosave' - -class TierCrowdsaleListStore { - - @observable crowdsaleList; - - constructor(crowdsaleList = []) { - this.crowdsaleList = crowdsaleList; - - autosave(this, 'TierCrowdsaleListStore') - } - - @action addCrowdsaleItem = (crowdsaleItem) => { - this.crowdsaleList.push(crowdsaleItem) - } - - @action setCrowdsaleItemProperty = (index, property, value) => { - let newCrowdsaleItem = {...this.crowdsaleList[index]} - newCrowdsaleItem[property] = value - this.crowdsaleList[index] = newCrowdsaleItem; - } - - @action removeCrowdsaleItem = (index) => { - this.crowdsaleList.splice(index,1) - } -} - -export default TierCrowdsaleListStore; diff --git a/src/stores/index.js b/src/stores/index.js index 9b1ebd596..5424607ae 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -2,12 +2,10 @@ import storage from 'store2' import ContractStore from './ContractStore'; import PricingStrategyStore from './PricingStrategyStore'; import ReservedTokenStore from './ReservedTokenStore'; -import StepThreeValidationStore from './StepThreeValidationStore'; import StepTwoValidationStore from './StepTwoValidationStore'; import TierStore from './TierStore'; import TokenStore from './TokenStore'; import Web3Store from './Web3Store'; -import TierCrowdsaleListStore from './TierCrowdsaleListStore' import CrowdsaleBlockListStore from './CrowdsaleBlockListStore' import GeneralStore from './GeneralStore' import CrowdsalePageStore from './CrowdsalePageStore' @@ -23,12 +21,10 @@ if (storage.has('DeploymentStore') && storage.get('DeploymentStore').deploymentS const generalStore = new GeneralStore() const crowdsalePageStore = new CrowdsalePageStore() -const tierCrowdsaleListStore = new TierCrowdsaleListStore() const crowdsaleBlockListStore = new CrowdsaleBlockListStore() const contractStore = new ContractStore() const pricingStrategyStore = new PricingStrategyStore() const reservedTokenStore = new ReservedTokenStore() -const stepThreeValidationStore = new StepThreeValidationStore() const stepTwoValidationStore = new StepTwoValidationStore() const tierStore = new TierStore() const tokenStore = new TokenStore() @@ -41,12 +37,10 @@ const deploymentStore = new DeploymentStore() export { generalStore, crowdsalePageStore, - tierCrowdsaleListStore, crowdsaleBlockListStore, contractStore, pricingStrategyStore, reservedTokenStore, - stepThreeValidationStore, stepTwoValidationStore, tierStore, tokenStore, From ce62095214818a259f02e2b341c953195b9fb2ed Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:08:09 -0300 Subject: [PATCH 10/24] Add enzyme-to-json --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index b8f39b6e6..46b2be64a 100755 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "coveralls": "^3.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-15": "^1.0.5", + "enzyme-to-json": "^3.3.1", "ethereumjs-testrpc": "^4.1.3", "ganache-cli": "^6.1.0-beta.0", "gulp": "^3.9.1", @@ -184,6 +185,9 @@ "web.jsx", "jsx", "node" + ], + "snapshotSerializers": [ + "enzyme-to-json/serializer" ] }, "babel": { From 35e6dd56ddf935d2fdb3cb7b41dd9bdcb7e4d11c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:09:10 -0300 Subject: [PATCH 11/24] Add bigNumber.js library --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 46b2be64a..946d8bf15 100755 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "babel-preset-stage-1": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-runtime": "6.23.0", + "bignumber.js": "^6.0.0", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", "classnames": "^2.2.5", From 56fa6d51fc98700ae5d902e4d6978bdb17181600 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:10:10 -0300 Subject: [PATCH 12/24] Add MockDate --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 946d8bf15..c07321676 100755 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "gulp-util": "^3.0.8", "markdown-toc": "^1.2.0", "mobx-react-devtools": "^4.2.15", + "mockdate": "^2.0.2", "react-test-renderer": "^15.6.2", "shelljs": "^0.7.8", "truffle": "^3.4.9", From 78f737f54bc5f041636f2087461c7d65500cd09f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:10:46 -0300 Subject: [PATCH 13/24] Update package-lock --- package-lock.json | 50 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3e205ad5..986eb8ad0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1704,6 +1704,11 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" }, + "bignumber.js": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-6.0.0.tgz", + "integrity": "sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA==" + }, "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", @@ -3827,7 +3832,7 @@ "lodash": "4.17.4", "object.assign": "4.1.0", "object.values": "1.0.4", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "enzyme-adapter-utils": { @@ -3838,7 +3843,16 @@ "requires": { "lodash": "4.17.4", "object.assign": "4.1.0", - "prop-types": "15.6.0" + "prop-types": "15.6.1" + } + }, + "enzyme-to-json": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz", + "integrity": "sha512-PrgRyZAgEwOrh5/8BtBWrwGcv1mC7yNohytIciAX6SUqDaXg1BlU8CepYQ9BgnDP1i1jTB65qJJITMMCph+T6A==", + "dev": true, + "requires": { + "lodash": "4.17.4" } }, "errno": { @@ -6278,7 +6292,7 @@ "fbjs": "0.8.16", "inline-style-prefixer": "3.0.8", "object-assign": "4.1.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "through": "2.3.8" } }, @@ -9875,6 +9889,12 @@ } } }, + "mockdate": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-2.0.2.tgz", + "integrity": "sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI=", + "dev": true + }, "moment": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", @@ -12224,9 +12244,9 @@ } }, "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", "requires": { "fbjs": "0.8.16", "loose-envify": "1.3.1", @@ -12284,7 +12304,7 @@ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-0.7.2.tgz", "integrity": "sha512-s1x+E3bsp0ojI8cHQ+czr+aG3huLZegH+tqAuRsXh6oXvzNfC+9L2PeFRBBu8eRBiejMRrRzSH7iwi5LDyWfRg==", "requires": { - "prop-types": "15.6.0", + "prop-types": "15.6.1", "qr.js": "0.0.0" } }, @@ -12440,7 +12460,7 @@ "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-alert": { @@ -12460,7 +12480,7 @@ "integrity": "sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA==", "requires": { "copy-to-clipboard": "3.0.8", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-countdown-clock": { @@ -12470,7 +12490,7 @@ "requires": { "coffeescript": "2.2.0", "create-react-class": "15.6.3", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-dev-utils": { @@ -12653,7 +12673,7 @@ "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-dropzone": { @@ -12662,7 +12682,7 @@ "integrity": "sha512-L/q6ySfhdG9Md3P21jFumzlm92TxRT0FtYX6G793Nf8bt7Fzpwx6gJsPk0idV094koj/Y5vRpp0q9+e0bdsjxw==", "requires": { "attr-accept": "1.1.2", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-error-overlay": { @@ -12713,7 +12733,7 @@ "invariant": "2.2.2", "loose-envify": "1.3.1", "path-to-regexp": "1.7.0", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "warning": "3.0.0" } }, @@ -12725,7 +12745,7 @@ "history": "4.7.2", "invariant": "2.2.2", "loose-envify": "1.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-router": "4.2.0", "warning": "3.0.0" } @@ -12748,7 +12768,7 @@ "chain-function": "1.0.0", "dom-helpers": "3.3.1", "loose-envify": "1.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "warning": "3.0.0" } }, From 5aa74326b1bd3470b8b1a216ea12faba75ead187 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:11:17 -0300 Subject: [PATCH 14/24] Remove validateRate method --- src/utils/utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/utils.js b/src/utils/utils.js index 3cb1616ce..9df79c2fe 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -82,8 +82,6 @@ export const validateLaterTime = (laterTime, previousTime) => getTimeAsNumber(la export const validateLaterOrEqualTime = (laterTime, previousTime) => getTimeAsNumber(laterTime) >= getTimeAsNumber(previousTime) -export const validateRate = (rate) => isNaN(Number(rate)) === false && Number(rate) > 0 - export const validateAddress = (address) => !(!address || address.length !== 42) export function toFixed(x) { From ede0916ab84ca305fad7595adcead7066378defc Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:12:53 -0300 Subject: [PATCH 15/24] Move defaultCompanyEndDate function --- src/components/stepThree/utils.js | 6 ++++++ src/utils/utils.js | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/stepThree/utils.js b/src/components/stepThree/utils.js index c20baa02d..39297dd87 100644 --- a/src/components/stepThree/utils.js +++ b/src/components/stepThree/utils.js @@ -5,3 +5,9 @@ export function defaultCompanyStartDate() { let crowdsaleStartDateFormatted = crowdsaleStartDate.format('YYYY-MM-DDTHH:mm'); return crowdsaleStartDateFormatted; } + +export function defaultCompanyEndDate(startDate) { + let endDate = new Date(startDate).setDate(new Date(startDate).getDate() + 4); + endDate = new Date(endDate).setUTCHours(0); + return new Date(endDate).toISOString().split(".")[0]; +} diff --git a/src/utils/utils.js b/src/utils/utils.js index 9df79c2fe..b7aae9442 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -102,12 +102,6 @@ export function toFixed(x) { return x; } -export function defaultCompanyEndDate(startDate) { - let endDate = new Date(startDate).setDate(new Date(startDate).getDate() + 4); - endDate = new Date(endDate).setUTCHours(0); - return new Date(endDate).toISOString().split(".")[0]; -} - export const toast = { msg: {}, showToaster: function ({ type = TOAST.TYPE.INFO, message = '', options = {} }) { From f25d90037a64eb5fe2ee0bdd466e9058ac57c0dd Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:15:10 -0300 Subject: [PATCH 16/24] Refactor defaultCompanyEndDate signature --- src/components/stepThree/utils.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/stepThree/utils.js b/src/components/stepThree/utils.js index 39297dd87..eb0ce2291 100644 --- a/src/components/stepThree/utils.js +++ b/src/components/stepThree/utils.js @@ -6,8 +6,7 @@ export function defaultCompanyStartDate() { return crowdsaleStartDateFormatted; } -export function defaultCompanyEndDate(startDate) { - let endDate = new Date(startDate).setDate(new Date(startDate).getDate() + 4); - endDate = new Date(endDate).setUTCHours(0); - return new Date(endDate).toISOString().split(".")[0]; +export const defaultCompanyEndDate = (startDate) => { + const crowdsaleEndDate = moment(startDate).add(4, 'days').startOf('day') + return crowdsaleEndDate.format('YYYY-MM-DDTHH:mm') } From d77ade39eab4d7d9b2174d6bc881d90c91f04ecf Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:19:29 -0300 Subject: [PATCH 17/24] Update '_oneTokenInWei' calculus --- src/stores/utils.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/stores/utils.js b/src/stores/utils.js index d6b583183..6bde38e6f 100644 --- a/src/stores/utils.js +++ b/src/stores/utils.js @@ -1,6 +1,6 @@ -import { TRUNC_TO_DECIMALS } from '../utils/constants' -import { floorToDecimals, setFlatFileContentToState, toFixed } from '../utils/utils' +import { setFlatFileContentToState, toFixed } from '../utils/utils' import { contractStore, tokenStore, tierStore, web3Store } from './index' +import { BigNumber } from 'bignumber.js' export function getWhiteListWithCapCrowdsaleAssets() { const contractsRoute = './contracts/' @@ -98,9 +98,10 @@ export const getconstructorParams = (abiConstructor, vals, crowdsaleNum, isCrowd params.vals.push(true); break; case "_oneTokenInWei": - let oneTokenInETHRaw = toFixed(1 / tierStore.tiers[crowdsaleNum].rate).toString() - let oneTokenInETH = floorToDecimals(TRUNC_TO_DECIMALS.DECIMALS18, oneTokenInETHRaw) - params.vals.push(web3Store.web3.utils.toWei(oneTokenInETH, "ether")); + BigNumber.config({ DECIMAL_PLACES: 18 }) + const rate = new BigNumber(tierStore.tiers[crowdsaleNum].rate) + const tokenInEther = rate.pow(-1).toFixed() + params.vals.push(web3Store.web3.utils.toWei(tokenInEther, "ether")) break; case "_isUpdatable": params.vals.push(tierStore.tiers[crowdsaleNum].updatable ? tierStore.tiers[crowdsaleNum].updatable==="on" ? true : false : false); From 149cc2231f86b2e1de89e40e4861cca5fd3dfb33 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:20:18 -0300 Subject: [PATCH 18/24] Update 'oneTokenInETH' calculus --- src/components/stepFour/utils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/stepFour/utils.js b/src/components/stepFour/utils.js index 85743f3e2..d7fc263a0 100644 --- a/src/components/stepFour/utils.js +++ b/src/components/stepFour/utils.js @@ -21,6 +21,7 @@ import { web3Store } from '../../stores' import { getEncodedABIClientSide } from '../../utils/microservices' +import { BigNumber } from 'bignumber.js' export const setupContractDeployment = () => { if (!contractStore.safeMathLib) { @@ -147,7 +148,9 @@ export const deployToken = () => { } const getPricingStrategyParams = tier => { - const oneTokenInETH = floorToDecimals(TRUNC_TO_DECIMALS.DECIMALS18, 1 / tier.rate) + BigNumber.config({ DECIMAL_PLACES: 18 }) + const rate = new BigNumber(tier.rate) + const oneTokenInETH = rate.pow(-1).toFixed() return [ web3Store.web3.utils.toWei(oneTokenInETH, 'ether') From a388a397b898b3895a0ccd59d7904fd8c8cdcf97 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:22:01 -0300 Subject: [PATCH 19/24] Add snapshots for tests --- .../__snapshots__/BigNumberInput.spec.js.snap | 40 + .../__snapshots__/CrowdsaleBlock.spec.js.snap | 1741 +++++++++++++++++ 2 files changed, 1781 insertions(+) create mode 100644 src/components/Common/__snapshots__/BigNumberInput.spec.js.snap create mode 100644 src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap diff --git a/src/components/Common/__snapshots__/BigNumberInput.spec.js.snap b/src/components/Common/__snapshots__/BigNumberInput.spec.js.snap new file mode 100644 index 000000000..1360efd4a --- /dev/null +++ b/src/components/Common/__snapshots__/BigNumberInput.spec.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BigNumberInput Should render the component 1`] = ` +
+ + +

+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens +

+

+

+`; diff --git a/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap b/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap new file mode 100644 index 000000000..f3a9f3c5f --- /dev/null +++ b/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap @@ -0,0 +1,1741 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CrowdsaleBlock Should render the component for the first Tier 1`] = ` + + + +
+
+
+ +
+ + +

+ Name of a tier, e.g. PrePreCrowdsale, PreCrowdsale, Crowdsale with bonus A, Crowdsale with bonus B, etc. We simplified that and will increment a number after each tier. +

+

+

+
+ +
+ +
+ + +
+

+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing. +

+
+
+
+
+ +
+ + +

+ Date and time when the tier starts. Can't be in the past from the current moment. +

+

+

+
+ +
+ + +

+ Date and time when the tier ends. Can be only in the future. +

+

+

+
+
+
+ + +
+ + +

+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens +

+

+

+
+
+ +
+ + +

+ How many tokens will be sold on this tier. Cap of crowdsale equals to sum of supply of all tiers +

+

+

+
+
+
+
+
+
+
+`; + +exports[`CrowdsaleBlock Should render the component for the second Tier 1`] = ` + + + +
+
+
+ +
+ + +

+ Name of a tier, e.g. PrePreCrowdsale, PreCrowdsale, Crowdsale with bonus A, Crowdsale with bonus B, etc. We simplified that and will increment a number after each tier. +

+

+

+
+ +
+ +
+ + +
+

+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing. +

+
+
+
+
+ +
+ + +

+ Date and time when the tier starts. Can't be in the past from the current moment. +

+

+

+
+ +
+ + +

+ Date and time when the tier ends. Can be only in the future. +

+

+

+
+
+
+ + +
+ + +

+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens +

+

+

+
+
+ +
+ + +

+ How many tokens will be sold on this tier. Cap of crowdsale equals to sum of supply of all tiers +

+

+

+
+
+
+
+
+
+
+`; + +exports[`CrowdsaleBlock Should render the component for the second Tier with whitelist enabled 1`] = ` + + + +
+
+
+ +
+ + +

+ Name of a tier, e.g. PrePreCrowdsale, PreCrowdsale, Crowdsale with bonus A, Crowdsale with bonus B, etc. We simplified that and will increment a number after each tier. +

+

+

+
+ +
+ +
+ + +
+

+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing. +

+
+
+
+
+ +
+ + +

+ Date and time when the tier starts. Can't be in the past from the current moment. +

+

+

+
+ +
+ + +

+ Date and time when the tier ends. Can be only in the future. +

+

+

+
+
+
+ + +
+ + +

+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens +

+

+

+
+
+ +
+ + +

+ How many tokens will be sold on this tier. Cap of crowdsale equals to sum of supply of all tiers +

+

+

+
+
+
+
+
+

+ Whitelist +

+
+ + +
+ +
+ + Upload CSV + + +
+
+
+
+ +
+ + +

+ Address of a whitelisted account. Whitelists are inherited. E.g., if an account whitelisted on Tier 1 and didn't buy max cap on Tier 1, he can buy on Tier 2, and following tiers. +

+

+

+
+ +
+ + +

+ Minimum amount tokens to buy. Not a minimal size of a transaction. If minCap is 1 and user bought 1 token in a previous transaction and buying 0.1 token it will allow him to buy. +

+

+

+
+ +
+ + +

+ Maximum is the hard limit. +

+

+

+
+
+
+
+
+
+
+ + +
+
+ + + +`; From ea732080a58b32577c1f18a15ecace44629e1fe7 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Mar 2018 16:33:33 -0300 Subject: [PATCH 20/24] Add test for stepThree utils --- src/components/stepThree/utils.spec.js | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/components/stepThree/utils.spec.js diff --git a/src/components/stepThree/utils.spec.js b/src/components/stepThree/utils.spec.js new file mode 100644 index 000000000..7b0e7e688 --- /dev/null +++ b/src/components/stepThree/utils.spec.js @@ -0,0 +1,41 @@ +import React from 'react' +import { defaultCompanyStartDate, defaultCompanyEndDate } from './utils' +import MockDate from 'mockdate' +import moment from 'moment' + +beforeEach(() => { + const currentTime = '2018-03-05T11:00:00' + MockDate.set(currentTime) +}) + +describe('defaultCompanyStartDate', () => { + it('Should return a day formatted as: YYYY-MM-DDTHH:mm', () => { + const isFormatOk = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/ + const startDate = defaultCompanyStartDate() + + expect(isFormatOk.test(startDate)).toBeTruthy() + }) + + it('Should return a day 5 minutes in the future', () => { + const startDate = defaultCompanyStartDate() + + expect(moment().add(5, 'minutes').isSame(startDate)).toBeTruthy() + }) +}) + +describe('defaultComanyEndDate', () => { + it('Should return a day formatted as: YYYY-MM-DDTHH:mm', () => { + const isFormatOk = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/ + const startDate = defaultCompanyStartDate() + const endDate = defaultCompanyEndDate(startDate) + + expect(isFormatOk.test(endDate)).toBeTruthy() + }) + + it('Should return a date 4 days in the future, at 00:00', () => { + const startDate = defaultCompanyStartDate() + const endDate = defaultCompanyEndDate(startDate) + + expect(moment().add(4, 'days').startOf('day').isSame(endDate)).toBeTruthy() + }) +}) From d57eb92d1284ff76568bbd3ecbe1674da8ed523a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 6 Mar 2018 09:31:45 -0300 Subject: [PATCH 21/24] Change attribute name --- src/components/manage/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/manage/index.js b/src/components/manage/index.js index 5369bc579..ca9ebfe57 100644 --- a/src/components/manage/index.js +++ b/src/components/manage/index.js @@ -514,7 +514,7 @@ export class Manage extends Component { side='left' type='text' title={CROWDSALE_SETUP_NAME} - value={tier.name} + value={tier.tier} disabled={true} /> Date: Tue, 6 Mar 2018 09:32:58 -0300 Subject: [PATCH 22/24] Remove no longer needed setup code for tierStore in manage screen --- src/components/manage/utils.js | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/components/manage/utils.js b/src/components/manage/utils.js index c084ae54e..1c5f62601 100644 --- a/src/components/manage/utils.js +++ b/src/components/manage/utils.js @@ -269,30 +269,16 @@ export const processTier = (crowdsaleAddress, crowdsaleNum) => { newTier.rate = (contractStore.contractType === WHITELIST_WITH_CAP) ? tokensPerETHTiers : tokensPerETHStandard - if (crowdsaleNum === 0) { - tierStore.emptyList() - tierStore.addTier(newTier) - tierStore.setTierProperty(newTier.tier, 'tier', crowdsaleNum) - tierStore.setTierProperty(newTier.walletAddress, 'walletAddress', crowdsaleNum) - tierStore.setTierProperty(newTier.rate, 'rate', crowdsaleNum) - tierStore.setTierProperty(newTier.supply, 'supply', crowdsaleNum) - tierStore.setTierProperty(newTier.startTime, 'startTime', crowdsaleNum) - tierStore.setTierProperty(newTier.endTime, 'endTime', crowdsaleNum) - tierStore.setTierProperty(newTier.updatable, 'updatable', crowdsaleNum) - tierStore.validateTiers('rate', crowdsaleNum) - tierStore.validateTiers('supply', crowdsaleNum) - } else { - tierStore.addTier(newTier) - tierStore.addTierValidations({ - tier: VALID, - walletAddress: VALID, - rate: VALID, - supply: VALID, - startTime: VALID, - endTime: VALID, - updatable: VALID - }) - } + tierStore.addTier(newTier) + tierStore.addTierValidations({ + tier: VALID, + walletAddress: VALID, + rate: VALID, + supply: VALID, + startTime: VALID, + endTime: VALID, + updatable: VALID + }) const whitelist = newTier.whitelist.slice() const whitelistElements = newTier.whitelistElements.slice() From b1b7c2b506871312e459446d743d7f1be61d7197 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 6 Mar 2018 12:46:25 -0300 Subject: [PATCH 23/24] Use BigNumber library for crowdsale page --- src/components/crowdsale/index.js | 135 ++++++++++++++---------------- src/components/crowdsale/utils.js | 30 +++---- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/src/components/crowdsale/index.js b/src/components/crowdsale/index.js index 3aa138d98..3240986ea 100644 --- a/src/components/crowdsale/index.js +++ b/src/components/crowdsale/index.js @@ -7,7 +7,8 @@ import { getContractStoreProperty, getCrowdsaleData, getJoinedTiers, - initializeAccumulativeData + initializeAccumulativeData, + toBigNumber } from './utils' import { getQueryVariable, toFixed } from '../../utils/utils' import { getWhiteListWithCapCrowdsaleAssets } from '../../stores/utils' @@ -120,120 +121,108 @@ export class Crowdsale extends React.Component { render() { const { web3Store, contractStore, tokenStore, crowdsalePageStore } = this.props const { web3 } = web3Store + + const isWhitelistWithCap = contractStore.contractType === CONTRACT_TYPES.whitelistwithcap + const tokenAddr = getContractStoreProperty('token','addr') const tempCrowdsaleAddr = getContractStoreProperty('crowdsale','addr') const crowdsaleAddr = tempCrowdsaleAddr === 'string' ? tempCrowdsaleAddr : tempCrowdsaleAddr[0] - const tokenDecimals = !isNaN(tokenStore.decimals) ? tokenStore.decimals : 0; - const rate = crowdsalePageStore.rate; //for tiers: 1 token in wei, for standard: 1/? 1 token in eth - const maxCapBeforeDecimals = crowdsalePageStore.maximumSellableTokens / 10**tokenDecimals; - const investorsCount = crowdsalePageStore.investors ? crowdsalePageStore.investors.toString() : 0; - const ethRaised = crowdsalePageStore.ethRaised; - - //tokens claimed: tiers, standard - const tokensClaimedStandard = rate ? (crowdsalePageStore.ethRaised / rate) : 0; - const tokensClaimedTiers = rate ? (crowdsalePageStore.tokensSold / 10**tokenDecimals) : 0; - const tokensClaimed = (contractStore.contractType === CONTRACT_TYPES.whitelistwithcap) ? tokensClaimedTiers : tokensClaimedStandard; + const investorsCount = crowdsalePageStore.investors ? crowdsalePageStore.investors.toString() : 0 + + const rate = toBigNumber(crowdsalePageStore.rate) //for tiers: 1 token in wei, for standard: 1/? 1 token in eth + const tokenDecimals = toBigNumber(tokenStore.decimals) + const maximumSellableTokens = toBigNumber(crowdsalePageStore.maximumSellableTokens) + const maximumSellableTokensInWei = toBigNumber(crowdsalePageStore.maximumSellableTokensInWei) + const ethRaised = toBigNumber(crowdsalePageStore.ethRaised) + const tokensSold = toBigNumber(crowdsalePageStore.tokensSold) + const supply = toBigNumber(crowdsalePageStore.supply) + const maxCapBeforeDecimals = maximumSellableTokens.div(`1e${tokenDecimals}`) + + // tokens claimed: tiers, standard + const tokensClaimedStandard = rate > 0 ? ethRaised.div(rate).toFixed() : '0' + const tokensClaimedTiers = tokensSold.div(`1e${tokenDecimals}`).toFixed() + const tokensClaimed = isWhitelistWithCap ? tokensClaimedTiers : tokensClaimedStandard //price: tiers, standard - const tokensPerETHStandard = !isNaN(rate) ? rate : 0; - const tokensPerETHTiers = !isNaN(1 / rate) ? 1 / web3.utils.fromWei(toFixed(rate).toString(), "ether") : 0; - const tokensPerETH = (contractStore.contractType === CONTRACT_TYPES.whitelistwithcap) ? tokensPerETHTiers : tokensPerETHStandard; + const rateInETH = toBigNumber(web3.utils.fromWei(rate.toFixed(), 'ether')) + const tokensPerETH = isWhitelistWithCap ? rateInETH.pow(-1).toFixed() : rate.toFixed() //total supply: tiers, standard - const tierCap = maxCapBeforeDecimals ? (maxCapBeforeDecimals).toString() : 0; - const standardCrowdsaleSupply = !isNaN(crowdsalePageStore.supply) ? (crowdsalePageStore.supply).toString() : 0; - const totalSupply = (contractStore.contractType === CONTRACT_TYPES.whitelistwithcap) ? tierCap : standardCrowdsaleSupply; + const tierCap = maxCapBeforeDecimals.toFixed() + const standardCrowdsaleSupply = supply.toFixed() + const totalSupply = isWhitelistWithCap ? tierCap : standardCrowdsaleSupply //goal in ETH - const goalInETHStandard = (totalSupply / rate).toExponential(); - let goalInETHTiers = crowdsalePageStore.maximumSellableTokensInWei ? (web3.utils.fromWei(toFixed(crowdsalePageStore.maximumSellableTokensInWei).toString(), "ether").toString()) : 0; - goalInETHTiers = 1.0 / 100 * Math.floor(100 * goalInETHTiers) - const goalInETH = (contractStore.contractType === CONTRACT_TYPES.whitelistwithcap) ? goalInETHTiers : goalInETHStandard; - - const tokensClaimedRatio = goalInETH ? (ethRaised / goalInETH) * 100 : "0"; + const goalInETHStandard = rate > 0 ? toBigNumber(totalSupply).div(rate).toFixed() : '0' + const goalInETHTiers = toBigNumber(web3.utils.fromWei(maximumSellableTokensInWei.toFixed(), 'ether')).toFixed() + const goalInETH = isWhitelistWithCap ? goalInETHTiers : goalInETHStandard + const tokensClaimedRatio = goalInETH > 0 ? ethRaised.div(goalInETH).times(100).toFixed() : '0' return (
- +
-
+

Crowdsale Page

-

- Page with statistics of crowdsale. Statistics for all tiers combined on the page. Please press Ctrl-D to bookmark the page. -

+

Page with statistics of crowdsale. Statistics for all tiers combined on the page. + Please press Ctrl-D to bookmark the page.

-

{ethRaised} ETH

-

- Total Raised Funds -

+

{`${ethRaised}`} ETH

+

Total Raised Funds

-

{goalInETH} ETH

-

- Goal -

+

{`${goalInETH}`} ETH

+

Goal

-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-

{tokensClaimed}

-

- Tokens Claimed -

+

{`${tokensClaimed}`}

+

Tokens Claimed

-

{investorsCount}

-

- Contributors -

+

{`${investorsCount}`}

+

Contributors

-

{tokenAddr}

-

- Token Address -

+

{`${tokenAddr}`}

+

Token Address

-
+
-

{tokensPerETH}

-

- Price (Tokens/ETH) -

+

{`${tokensPerETH}`}

+

Price (Tokens/ETH)

-

{totalSupply}

-

- Total Supply -

+

{`${totalSupply}`}

+

Total Supply

-

{crowdsaleAddr}

-

- Crowdsale Contract Address -

+

{`${crowdsaleAddr}`}

+

Crowdsale Contract Address

@@ -241,7 +230,7 @@ export class Crowdsale extends React.Component { - +
) } diff --git a/src/components/crowdsale/utils.js b/src/components/crowdsale/utils.js index 6dcd123c9..e55067a7a 100644 --- a/src/components/crowdsale/utils.js +++ b/src/components/crowdsale/utils.js @@ -4,6 +4,12 @@ import { toFixed } from '../../utils/utils' import { CONTRACT_TYPES } from '../../utils/constants' import { contractStore, crowdsalePageStore, tokenStore, web3Store } from '../../stores' import { toJS } from 'mobx' +import { BigNumber } from 'bignumber.js' + +BigNumber.config({ DECIMAL_PLACES : 18 }) + +export const toBigNumber = (value) => isNaN(value) || value === '' ? new BigNumber(0) : new BigNumber(value) + export function getJoinedTiers(abi, addr, joinedCrowdsales, cb) { attachToContract(abi, addr) @@ -236,12 +242,8 @@ export function getAccumulativeCrowdsaleData() { }) let getMaximumSellableTokens = crowdsaleContract.methods.maximumSellableTokens().call().then((maximumSellableTokens) => { - const maxSellableTokens = crowdsalePageStore.maximumSellableTokens - if (maxSellableTokens) { - crowdsalePageStore.setProperty('maximumSellableTokens', maxSellableTokens + parseInt(toFixed(maximumSellableTokens), 10)) - } else { - crowdsalePageStore.setProperty('maximumSellableTokens', parseInt(toFixed(maximumSellableTokens), 10)) - } + const maxSellableTokens = toBigNumber(crowdsalePageStore.maximumSellableTokens) + crowdsalePageStore.setProperty('maximumSellableTokens', maxSellableTokens.plus(maximumSellableTokens).toFixed()) //calc maximumSellableTokens in Eth return setMaximumSellableTokensInEth(crowdsaleContract, maximumSellableTokens) @@ -273,13 +275,13 @@ function setMaximumSellableTokensInEth(crowdsaleContract, maximumSellableTokens) .then(pricingStrategyContract => { if (!pricingStrategyContract) return noContractAlert() - return pricingStrategyContract.methods.oneTokenInWei().call().then((oneTokenInWei) => { - if (crowdsalePageStore.maximumSellableTokensInWei) { - crowdsalePageStore.setProperty('maximumSellableTokensInWei', crowdsalePageStore.maximumSellableTokensInWei + parseInt(oneTokenInWei, 10) * maximumSellableTokens / 10 ** tokenStore.decimals) - } else { - crowdsalePageStore.setProperty('maximumSellableTokensInWei', parseInt(oneTokenInWei, 10) * maximumSellableTokens / 10 ** tokenStore.decimals) - } - }) + return pricingStrategyContract.methods.oneTokenInWei().call() + .then((oneTokenInWei) => { + const currentMaximumSellableTokensInWei = toBigNumber(crowdsalePageStore.maximumSellableTokensInWei) + const maximumSellableTokensInWei = toBigNumber(oneTokenInWei).times(maximumSellableTokens).div(`1e${tokenStore.decimals}`).dp(0) + + crowdsalePageStore.setProperty('maximumSellableTokensInWei', currentMaximumSellableTokensInWei.plus(maximumSellableTokensInWei).toFixed()) + }) }) } @@ -644,7 +646,7 @@ export function getPricingStrategyData () { } console.log('pricing strategy rate:', rate) - crowdsalePageStore.setProperty('rate', parseInt(rate, 10)) + crowdsalePageStore.setProperty('rate', rate) resolve() }) }) From 4b92529bd33bae016a1f7d5c913c9a03c7960f0a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 6 Mar 2018 13:54:22 -0300 Subject: [PATCH 24/24] Update snapshot --- .../__snapshots__/CrowdsaleBlock.spec.js.snap | 115 +++++++++--------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap b/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap index f3a9f3c5f..a4e132eac 100644 --- a/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap +++ b/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap @@ -1531,66 +1531,6 @@ exports[`CrowdsaleBlock Should render the component for the second Tier with whi
- -
- - Upload CSV - - -
-
@@ -1730,6 +1670,61 @@ exports[`CrowdsaleBlock Should render the component for the second Tier with whi />
+ +
+ +   Upload CSV + +
+