diff --git a/README.md b/README.md
index 9e49846f5..fc6e6c708 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ __Deploying on POA's Sokol or Main network is much faster then using Ethereum's
- [Strategy](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Strategy)
- [How to run](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-How-to-run)
- [Using POA Networks](https://github.com/poanetwork/token-wizard/wiki/Connecting-to-POA-Networks)
-- [Deployment Time & Gas](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Deployment-Stats)
+- [Deployment Time & Gas](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Deployment-Time-and-Gas)
- [Verifying Smart-Contracts](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Verifying-Contracts)
- [Projects on Token Wizard](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Projects)
- [Notable Contributors](https://github.com/poanetwork/token-wizard/wiki/Token-Wizard-Notable-Contributors)
diff --git a/package-lock.json b/package-lock.json
index d28ab357e..8d75e677b 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",
@@ -3841,6 +3846,15 @@
"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": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz",
@@ -9880,6 +9894,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",
diff --git a/package.json b/package.json
index e88f80509..d8d79c5b9 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",
@@ -26,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",
@@ -43,6 +45,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",
@@ -187,6 +190,9 @@
"web.jsx",
"jsx",
"node"
+ ],
+ "snapshotSerializers": [
+ "enzyme-to-json/serializer"
]
},
"babel": {
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 (
+
+ )
+ }
+}
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
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/src/components/Common/WhitelistInputBlock.js b/src/components/Common/WhitelistInputBlock.js
index bf9fc06fc..33ade245c 100644
--- a/src/components/Common/WhitelistInputBlock.js
+++ b/src/components/Common/WhitelistInputBlock.js
@@ -6,9 +6,10 @@ import Papa from 'papaparse'
import '../../assets/stylesheets/application.css';
import { InputField } from './InputField'
import { TEXT_FIELDS, VALIDATION_TYPES } from '../../utils/constants'
-import { validateAddress } from '../../utils/utils'
import { WhitelistItem } from './WhitelistItem'
import { inject, observer } from 'mobx-react'
+import { whitelistImported } from '../../utils/alerts'
+import processWhitelist from '../../utils/processWhitelist'
const { ADDRESS, MIN, MAX } = TEXT_FIELDS
const {VALID, INVALID} = VALIDATION_TYPES;
@@ -80,23 +81,16 @@ export class WhitelistInputBlock extends React.Component {
this.setState(newState)
}
- isAddress = (address) => validateAddress(address)
- isNumber = (number) => !isNaN(parseFloat(number))
-
onDrop = (acceptedFiles, rejectedFiles) => {
acceptedFiles.forEach(file => {
Papa.parse(file, {
skipEmptyLines: true,
complete: results => {
- results.data.forEach((row) => {
- if (row.length !== 3) return
-
- const [addr, min, max] = row
-
- if (!this.isAddress(addr) || !this.isNumber(min) || !this.isNumber(max)) return
-
- this.props.tierStore.addWhitelistItem({ addr, min, max }, this.props.num)
+ const { called } = processWhitelist(results.data, item => {
+ this.props.tierStore.addWhitelistItem(item, this.props.num)
})
+
+ whitelistImported(called)
}
})
})
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`] = `
+
+
+ Rate
+
+
+
+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens
+
+
+
+`;
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()
})
})
diff --git a/src/components/manage/index.js b/src/components/manage/index.js
index 5ff2fcb52..d85017850 100644
--- a/src/components/manage/index.js
+++ b/src/components/manage/index.js
@@ -459,7 +459,7 @@ export class Manage extends Component {
tierHasStarted = (index) => {
const initialTierValues = this.props.crowdsaleStore.selected.initialTiersValues[index]
- return initialTierValues ? Date.now() > new Date(initialTierValues.startTime).getTime() : true
+ return initialTierValues && new Date(initialTierValues.startTime).getTime() < Date.now()
}
tierHasEnded = (index) => {
@@ -471,7 +471,8 @@ export class Manage extends Component {
const { formPristine, canFinalize, shouldDistribute, canDistribute, crowdsaleHasEnded, ownerCurrentUser } = this.state
const { generalStore, tierStore, tokenStore, crowdsaleStore } = this.props
const { address: crowdsaleAddress, finalized, updatable } = crowdsaleStore.selected
- let disabled = !ownerCurrentUser || canDistribute || canFinalize || finalized
+
+ const canEditTier = ownerCurrentUser && !canDistribute && !canFinalize && !finalized
const distributeTokensStep = (
@@ -527,7 +528,7 @@ export class Manage extends Component {
side='left'
type='text'
title={CROWDSALE_SETUP_NAME}
- value={tier.name}
+ value={tier.tier}
disabled={true}
/>
{
- disabled = disabled || !tier.updatable || this.tierHasEnded(index)
+ const disabled = !canEditTier || !tier.updatable || this.tierHasEnded(index)
return
{
- disabled = disabled || !tier.updatable || this.tierHasEnded(index) || this.tierHasStarted(index)
+ const disabled = !canEditTier || !tier.updatable || this.tierHasEnded(index) || this.tierHasStarted(index)
return
{
newTier.startTime = formatDate(startsAt)
newTier.endTime = formatDate(endsAt)
newTier.updatable = updatable
- newTier.name = name
+ newTier.tier = name
initialValues.updatable = newTier.updatable
initialValues.index = crowdsaleNum
@@ -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()
diff --git a/src/components/stepFour/index.js b/src/components/stepFour/index.js
index a9bfd27d5..a80243b8c 100644
--- a/src/components/stepFour/index.js
+++ b/src/components/stepFour/index.js
@@ -48,6 +48,7 @@ export class stepFour extends React.Component {
transactionFailed: false
}
+ this.props.deploymentStore.setDeploymentStep(0)
this.props.deploymentStore.setDeployerAccount(context.selectedAccount)
}
diff --git a/src/components/stepFour/utils.js b/src/components/stepFour/utils.js
index c02e571f3..699880951 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 = (web3) => {
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')
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 = (
- );
+ )
+
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 : ''}
- );
+ )
}
}
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)
+ })
+})
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..a4e132eac
--- /dev/null
+++ b/src/components/stepThree/__snapshots__/CrowdsaleBlock.spec.js.snap
@@ -0,0 +1,1736 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CrowdsaleBlock Should render the component for the first Tier 1`] = `
+
+
+
+
+
+
+
+
+
+ Crowdsale setup name
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Allow modifying
+
+
+
+
+
+ on
+
+
+
+
+
+ off
+
+
+
+
+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing.
+
+
+
+
+
+
+
+
+ Start Time
+
+
+
+ Date and time when the tier starts. Can't be in the past from the current moment.
+
+
+
+
+
+
+
+ End Time
+
+
+
+ Date and time when the tier ends. Can be only in the future.
+
+
+
+
+
+
+
+
+
+
+ Rate
+
+
+
+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens
+
+
+
+
+
+
+
+
+ Supply
+
+
+
+ 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`] = `
+
+
+
+
+
+
+
+
+
+ Crowdsale setup name
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Allow modifying
+
+
+
+
+
+ on
+
+
+
+
+
+ off
+
+
+
+
+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing.
+
+
+
+
+
+
+
+
+ Start Time
+
+
+
+ Date and time when the tier starts. Can't be in the past from the current moment.
+
+
+
+
+
+
+
+ End Time
+
+
+
+ Date and time when the tier ends. Can be only in the future.
+
+
+
+
+
+
+
+
+
+
+ Rate
+
+
+
+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens
+
+
+
+
+
+
+
+
+ Supply
+
+
+
+ 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`] = `
+
+
+
+
+
+
+
+
+
+ Crowdsale setup name
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Allow modifying
+
+
+
+
+
+ on
+
+
+
+
+
+ off
+
+
+
+
+ Pandora box feature. If it's enabled, a creator of the crowdsale can modify Start time, End time, Rate, Limit after publishing.
+
+
+
+
+
+
+
+
+ Start Time
+
+
+
+ Date and time when the tier starts. Can't be in the past from the current moment.
+
+
+
+
+
+
+
+ End Time
+
+
+
+ Date and time when the tier ends. Can be only in the future.
+
+
+
+
+
+
+
+
+
+
+ Rate
+
+
+
+ Exchange rate Ethereum to Tokens. If it's 100, then for 1 Ether you can buy 100 tokens
+
+
+
+
+
+
+
+
+ Supply
+
+
+
+ How many tokens will be sold on this tier. Cap of crowdsale equals to sum of supply of all tiers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Min
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Max
+
+
+
+ Maximum is the hard limit.
+
+
+
+
+
+
+
+
+
+
+ Upload CSV
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js
index 9d2aa547e..5f8aa388a 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,79 +69,74 @@ 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
const isMinCapValid = tierStore.globalMinCap <= tierStore.maxSupply
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 (!isMinCapValid) {
@@ -188,21 +181,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 }) {
@@ -232,23 +229,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
@@ -332,7 +312,8 @@ export class stepThree extends React.Component {
render() {
const { contractStore, crowdsaleBlockListStore, tierStore } = this.props;
- let globalSettingsBlock = (
+
+ const globalSettingsBlock = (
Global settings
@@ -342,11 +323,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()}
@@ -354,8 +336,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 = (
-
- );
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
+
+
- );
+ )
}
}
}
diff --git a/src/components/stepThree/utils.js b/src/components/stepThree/utils.js
index c20baa02d..eb0ce2291 100644
--- a/src/components/stepThree/utils.js
+++ b/src/components/stepThree/utils.js
@@ -5,3 +5,8 @@ export function defaultCompanyStartDate() {
let crowdsaleStartDateFormatted = crowdsaleStartDate.format('YYYY-MM-DDTHH:mm');
return crowdsaleStartDateFormatted;
}
+
+export const defaultCompanyEndDate = (startDate) => {
+ const crowdsaleEndDate = moment(startDate).add(4, 'days').startOf('day')
+ return crowdsaleEndDate.format('YYYY-MM-DDTHH:mm')
+}
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()
+ })
+})
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/TierStore.js b/src/stores/TierStore.js
index bcd1d2a12..f7a7e7506 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
+ 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':
diff --git a/src/stores/index.js b/src/stores/index.js
index b00133074..75b6b9616 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()
@@ -60,12 +56,10 @@ window.stores = {
export {
generalStore,
crowdsalePageStore,
- tierCrowdsaleListStore,
crowdsaleBlockListStore,
contractStore,
pricingStrategyStore,
reservedTokenStore,
- stepThreeValidationStore,
stepTwoValidationStore,
tierStore,
tokenStore,
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);
diff --git a/src/utils/alerts.js b/src/utils/alerts.js
index c8d4dbfdb..886846269 100644
--- a/src/utils/alerts.js
+++ b/src/utils/alerts.js
@@ -213,3 +213,10 @@ export function skippingTransaction() {
reverseButtons: true
})
}
+export function whitelistImported(count) {
+ return sweetAlert2({
+ title: 'Addresses imported',
+ html: `${count} addresses were added to the whitelist`,
+ type: 'info'
+ })
+}
diff --git a/src/utils/constants.js b/src/utils/constants.js
index 3271acfbd..46aca01c1 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,
diff --git a/src/utils/processWhitelist.js b/src/utils/processWhitelist.js
new file mode 100644
index 000000000..6d220e2e0
--- /dev/null
+++ b/src/utils/processWhitelist.js
@@ -0,0 +1,29 @@
+import Web3 from 'web3'
+
+const isNumber = (number) => !isNaN(parseFloat(number))
+
+/**
+ * Execute a callback with each valid whitelist item in the given list
+ *
+ * @param {Array} rows Array of whitelist items. Each element in the array has the structure `[address, min, max]`, for
+ * example: `['0x1234567890123456789012345678901234567890', '1', '10']`
+ * @param {Function} cb The function to be called with each valid item
+ * @returns {Object} Object with a `called` property, indicating the number of times the callback was called
+ */
+export default function (rows, cb) {
+ let called = 0
+ rows.forEach((row) => {
+ if (row.length !== 3) return
+
+ const [addr, min, max] = row
+
+ if (!Web3.utils.isAddress(addr) || !isNumber(min) || !isNumber(max)) return
+
+ cb({ addr, min, max })
+
+ called++
+ })
+
+ return { called }
+}
+
diff --git a/src/utils/processWhitelist.spec.js b/src/utils/processWhitelist.spec.js
new file mode 100644
index 000000000..b03d9fa42
--- /dev/null
+++ b/src/utils/processWhitelist.spec.js
@@ -0,0 +1,91 @@
+import processWhitelist from './processWhitelist'
+
+describe('processWhitelist function', () => {
+ it('should call the callback for each whitelist item', () => {
+ // Given
+ const rows = [
+ ['0x1111111111111111111111111111111111111111', '1', '10'],
+ ['0x2222222222222222222222222222222222222222', '1', '10'],
+ ['0x3333333333333333333333333333333333333333', '1', '10']
+ ]
+ const cb = jest.fn()
+
+ // When
+ processWhitelist(rows, cb)
+
+ // Then
+ expect(cb).toHaveBeenCalledTimes(3)
+ expect(cb.mock.calls[0]).toEqual([{ addr: rows[0][0], min: rows[0][1], max: rows[0][2] }])
+ expect(cb.mock.calls[1]).toEqual([{ addr: rows[1][0], min: rows[1][1], max: rows[1][2] }])
+ expect(cb.mock.calls[2]).toEqual([{ addr: rows[2][0], min: rows[2][1], max: rows[2][2] }])
+ })
+
+ it('should ignore items that don\t have 3 elements', () => {
+ // Given
+ const rows = [
+ ['1', '10'],
+ ['0x2222222222222222222222222222222222222222', '10'],
+ ['0x3333333333333333333333333333333333333333', '1'],
+ ['0x4444444444444444444444444444444444444444'],
+ [],
+ ['0x4444444444444444444444444444444444444444', '1', '10', '100'],
+ ]
+ const cb = jest.fn()
+
+ // When
+ processWhitelist(rows, cb)
+
+ // Then
+ expect(cb).toHaveBeenCalledTimes(0)
+ })
+
+ it('should return the number of times the callback was called', () => {
+ // Given
+ const rows = [
+ ['0x1111111111111111111111111111111111111111', '1', '10'],
+ ['0x2222222222222222222222222222222222222222', '1', '10'],
+ ['0x3333333333333333333333333333333333333333', '1', '10']
+ ]
+ const cb = jest.fn()
+
+ // When
+ const { called } = processWhitelist(rows, cb)
+
+ // Then
+ expect(called).toBe(3)
+ })
+
+ it('should ignore invalid numbers', () => {
+ // Given
+ const rows = [
+ ['0x1111111111111111111111111111111111111111', 'foo', '10'],
+ ['0x2222222222222222222222222222222222222222', '1', 'bar'],
+ ['0x3333333333333333333333333333333333333333', '', '10'],
+ ['0x4444444444444444444444444444444444444444', '1', '']
+ ]
+ const cb = jest.fn()
+
+ // When
+ const { called } = processWhitelist(rows, cb)
+
+ // Then
+ expect(called).toBe(0)
+ })
+
+ it('should ignore invalid addresses', () => {
+ // Given
+ const rows = [
+ ['0x123456789012345678901234567890123456789', '1', '10'], // 41 characters
+ ['0x12345678901234567890123456789012345678901', '1', '10'], // 43 characters
+ ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9CG', '1', '10'], // invalid character
+ ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9c1', '1', '10'] // invalid checksum
+ ]
+ const cb = jest.fn()
+
+ // When
+ const { called } = processWhitelist(rows, cb)
+
+ // Then
+ expect(called).toBe(0)
+ })
+})
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 3cb1616ce..cab7febe6 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -70,7 +70,7 @@ export const getStepClass = (step, activeStep) => step === activeStep ? "step-na
export const validateTier = (tier) => typeof tier === 'string' && tier.length > 0 && tier.length < 30
-export const validateName = (name) => typeof name === 'string' && name.length > 0 && name.length < 30
+export const validateName = (name) => typeof name === 'string' && name.length > 0 && name.length <= 30
export const validateSupply = (supply) => isNaN(Number(supply)) === false && Number(supply) > 0
@@ -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) {
@@ -104,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 = {} }) {
diff --git a/src/utils/utils.spec.js b/src/utils/utils.spec.js
index 29e75bde7..de24df6f5 100644
--- a/src/utils/utils.spec.js
+++ b/src/utils/utils.spec.js
@@ -1,4 +1,4 @@
-import { countDecimalPlaces, validateTicker } from './utils'
+import { countDecimalPlaces, validateName, validateTicker } from './utils'
describe('countDecimalPlaces', () => {
[
@@ -55,3 +55,22 @@ describe('validateTicker', () => {
})
})
})
+
+describe('validateName', () => {
+ [
+ {value: '', expected: false},
+ {value: 'T', expected: true},
+ {value: 'MyToken', expected: true},
+ {value: '123456789012345678901234567890', expected: true},
+ {value: '1234567890123456789012345678901', expected: false},
+ {value: 23, expected: false},
+ {value: ['my', 'token'], expected: false},
+ {value: { a: 1 }, expected: false},
+ ].forEach(testCase => {
+ const action = testCase.expected ? 'pass' : 'fail'
+
+ it(`Should ${action} for '${testCase.value}'`, () => {
+ expect(validateName(testCase.value)).toBe(testCase.expected)
+ })
+ })
+})
diff --git a/submodules/token-wizard-test-automation b/submodules/token-wizard-test-automation
index 483a59572..8202c9a77 160000
--- a/submodules/token-wizard-test-automation
+++ b/submodules/token-wizard-test-automation
@@ -1 +1 @@
-Subproject commit 483a595722c15b24eba6e5fb050a0e37d20eb304
+Subproject commit 8202c9a771bac51f914ec57cae8ff6487ae59007