From 371216367fe850423b42d211b50bfdbd704b5c40 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Tue, 10 Apr 2018 18:08:16 -0300 Subject: [PATCH] Add bulk import for reserved tokens --- .../Common/ReservedTokensInputBlock.js | 59 +++- .../Common/ReservedTokensInputBlock.spec.js | 1 + .../ReservedTokensInputBlock.spec.js.snap | 50 ++- src/components/stepTwo/StepTwoForm.spec.js | 19 +- .../__snapshots__/StepTwoForm.spec.js.snap | 320 ------------------ src/utils/alerts.js | 8 + src/utils/processReservedTokens.js | 32 ++ src/utils/processReservedTokens.spec.js | 111 ++++++ 8 files changed, 240 insertions(+), 360 deletions(-) delete mode 100644 src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap create mode 100644 src/utils/processReservedTokens.js create mode 100644 src/utils/processReservedTokens.spec.js diff --git a/src/components/Common/ReservedTokensInputBlock.js b/src/components/Common/ReservedTokensInputBlock.js index ee66bcd22..60590b824 100644 --- a/src/components/Common/ReservedTokensInputBlock.js +++ b/src/components/Common/ReservedTokensInputBlock.js @@ -1,5 +1,7 @@ import React, { Component } from 'react' import Web3 from 'web3' +import Dropzone from 'react-dropzone'; +import Papa from 'papaparse' import '../../assets/stylesheets/application.css' import { InputField } from './InputField' import { RadioInputField } from './RadioInputField' @@ -8,6 +10,8 @@ import update from 'immutability-helper' import ReservedTokensItem from './ReservedTokensItem' import { observer } from 'mobx-react'; import { NumericInput } from './NumericInput' +import { reservedTokensImported } from '../../utils/alerts' +import processReservedTokens from '../../utils/processReservedTokens' const { VALID, INVALID } = VALIDATION_TYPES const { ADDRESS, DIMENSION, VALUE } = TEXT_FIELDS @@ -130,6 +134,21 @@ export class ReservedTokensInputBlock extends Component { this.setState(newState) } + onDrop = (acceptedFiles, rejectedFiles) => { + acceptedFiles.forEach(file => { + Papa.parse(file, { + skipEmptyLines: true, + complete: results => { + const { called } = processReservedTokens(results.data, item => { + this.props.addReservedTokensItem(item) + }) + + reservedTokensImported(called) + } + }) + }) + } + render () { const reservedTokensElements = this.props.tokens.map((token, index) => { return ( @@ -165,8 +184,19 @@ export class ReservedTokensInputBlock extends Component { console.error(`unrecognized dimension '${this.state.dim}'`) } + const actionsStyle = { + textAlign: 'right' + } + const clearAllStyle = { - textAlign: 'right', + display: 'inline-block', + cursor: 'pointer' + } + + const dropzoneStyle = { + display: 'inline-block', + marginLeft: '1em', + position: 'relative', cursor: 'pointer' } @@ -210,13 +240,26 @@ export class ReservedTokensInputBlock extends Component { {reservedTokensElements} - { - tokensListEmpty ? null : ( -
-  Clear All -
- ) - } + + {/* Actions */} +
+ { + tokensListEmpty ? null : ( +
+  Clear All +
+ ) + } + + +   + Upload CSV + +
) } diff --git a/src/components/Common/ReservedTokensInputBlock.spec.js b/src/components/Common/ReservedTokensInputBlock.spec.js index 3c183931e..a7cc9a07e 100644 --- a/src/components/Common/ReservedTokensInputBlock.spec.js +++ b/src/components/Common/ReservedTokensInputBlock.spec.js @@ -8,6 +8,7 @@ import { configure, mount } from 'enzyme' const { VALID, INVALID } = VALIDATION_TYPES configure({ adapter: new Adapter() }) +jest.mock('react-dropzone', () => () =>Dropzone); describe('ReservedTokensInputBlock', () => { let tokenList diff --git a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap index cdfa3fbc7..fdffb367b 100644 --- a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap +++ b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap @@ -269,19 +269,30 @@ exports[`percentage 1`] = `
- -  Clear All +
+ +  Clear All +
+ + Dropzone +
`; @@ -555,19 +566,30 @@ exports[`tokens 1`] = `
- -  Clear All +
+ +  Clear All +
+ + Dropzone +
`; diff --git a/src/components/stepTwo/StepTwoForm.spec.js b/src/components/stepTwo/StepTwoForm.spec.js index 36ff7f38b..e47966a59 100644 --- a/src/components/stepTwo/StepTwoForm.spec.js +++ b/src/components/stepTwo/StepTwoForm.spec.js @@ -1,30 +1,13 @@ import React from 'react' import { Form } from 'react-final-form' import { StepTwoForm } from './StepTwoForm' -import renderer from 'react-test-renderer' import Adapter from 'enzyme-adapter-react-15' import { configure, mount } from 'enzyme' configure({ adapter: new Adapter() }) +jest.mock('react-dropzone', () => () =>Dropzone); describe('StepTwoForm', () => { - it('Should render the component', () => { - const component = renderer.create( -
- ) - - const tree = component.toJSON() - expect(tree).toMatchSnapshot() - }) - it('Should call onSubmit if all values are valid', () => { const onSubmit = jest.fn() const wrapper = mount( diff --git a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap b/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap deleted file mode 100644 index 1f6ea3cc8..000000000 --- a/src/components/stepTwo/__snapshots__/StepTwoForm.spec.js.snap +++ /dev/null @@ -1,320 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StepTwoForm Should render the component 1`] = ` - -
-
- - -

- The name of your token. Will be used by Etherscan and other tokenbrowsers. Be afraid of trademarks. -

- -

- -

-
- - -

- The five letter ticker for your token. There are 11,881,376 combinations for 26 english letters. - Be hurry. -

- -

- -

-
- - -

- Refers to how divisible a token can be, from 0 (not at all divisible) to 18 (pretty much continuous). -

- -

-

-

- -

-
-
-

- Reserved tokens -

-
-
-
-
-
- - -

- Address where to send reserved tokens. -

-

-

-
- -
- - -
-

- Fixed amount or % of crowdsaled tokens. Will be deposited to the account after finalization of the crowdsale. -

-
-
- - -

- Value in tokens. Don't forget to click + button for each reserved token. -

-

-

-
-
-
-
-
-
- - -`; diff --git a/src/utils/alerts.js b/src/utils/alerts.js index ed1f3668a..08f9c1feb 100644 --- a/src/utils/alerts.js +++ b/src/utils/alerts.js @@ -237,3 +237,11 @@ export function whitelistImported(count) { type: 'info' }) } + +export function reservedTokensImported(count) { + return sweetAlert2({ + title: 'Reserved tokens imported', + html: `Tokens will be reserved for ${count} addresses`, + type: 'info' + }) +} diff --git a/src/utils/processReservedTokens.js b/src/utils/processReservedTokens.js new file mode 100644 index 000000000..15645310f --- /dev/null +++ b/src/utils/processReservedTokens.js @@ -0,0 +1,32 @@ +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 reserved tokens items. Each element in the array has the structure `[address, dim, val]`, for + * example: `['0x1234567890123456789012345678901234567890', 'tokens', '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, dim, val] = row + + if (!Web3.utils.isAddress(addr) || !dim || !isNumber(val)) return + + // `dim` must be either 'tokens' or 'percentage' + if (!['tokens', 'percentage'].includes(dim.toLowerCase())) return + + cb({ addr, dim, val }) + + called++ + }) + + return { called } +} + diff --git a/src/utils/processReservedTokens.spec.js b/src/utils/processReservedTokens.spec.js new file mode 100644 index 000000000..8664a6666 --- /dev/null +++ b/src/utils/processReservedTokens.spec.js @@ -0,0 +1,111 @@ +import processReservedTokens from './processReservedTokens' + +describe('processReservedTokens function', () => { + it('should call the callback for each reserved tokens item', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', 'tokens', '10'], + ['0x2222222222222222222222222222222222222222', 'tokens', '10'], + ['0x2222222222222222222222222222222222222222', 'percentage', '10'], + ['0x3333333333333333333333333333333333333333', 'tokens', '10'] + ] + const cb = jest.fn() + + // When + processReservedTokens(rows, cb) + + // Then + expect(cb).toHaveBeenCalledTimes(4) + expect(cb.mock.calls[0]).toEqual([{ addr: rows[0][0], dim: rows[0][1], val: rows[0][2] }]) + expect(cb.mock.calls[1]).toEqual([{ addr: rows[1][0], dim: rows[1][1], val: rows[1][2] }]) + expect(cb.mock.calls[2]).toEqual([{ addr: rows[2][0], dim: rows[2][1], val: rows[2][2] }]) + expect(cb.mock.calls[3]).toEqual([{ addr: rows[3][0], dim: rows[3][1], val: rows[3][2] }]) + }) + + it('should ignore items that don\t have 3 elements', () => { + // Given + const rows = [ + ['1', '10'], + ['0x2222222222222222222222222222222222222222', '10'], + ['0x3333333333333333333333333333333333333333', 'tokens'], + ['0x4444444444444444444444444444444444444444'], + [], + ['0x4444444444444444444444444444444444444444', 'percentage', '10', '100'], + ] + const cb = jest.fn() + + // When + processReservedTokens(rows, cb) + + // Then + expect(cb).toHaveBeenCalledTimes(0) + }) + + it('should return the number of times the callback was called', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', 'tokens', '10'], + ['0x2222222222222222222222222222222222222222', 'tokens', '10'], + ['0x2222222222222222222222222222222222222222', 'percentage', '10'], + ['0x3333333333333333333333333333333333333333', 'tokens', '10'] + ] + const cb = jest.fn() + + // When + const { called } = processReservedTokens(rows, cb) + + // Then + expect(called).toBe(4) + }) + + it('should ignore invalid numbers', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', 'tokens', 'foo'], + ['0x2222222222222222222222222222222222222222', 'tokens', ''], + ['0x2222222222222222222222222222222222222222', 'percentage', 'bar'], + ['0x3333333333333333333333333333333333333333', 'percentage', ''] + ] + const cb = jest.fn() + + // When + const { called } = processReservedTokens(rows, cb) + + // Then + expect(called).toBe(0) + }) + + it('should ignore invalid addresses', () => { + // Given + const rows = [ + ['0x123456789012345678901234567890123456789', 'tokens', '10'], // 41 characters + ['0x12345678901234567890123456789012345678901', 'tokens', '10'], // 43 characters + ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9CG', 'tokens', '10'], // invalid character + ['0x90F8bf6A479f320ead074411a4B0e7944Ea8c9c1', 'tokens', '10'] // invalid checksum + ] + const cb = jest.fn() + + // When + const { called } = processReservedTokens(rows, cb) + + // Then + expect(called).toBe(0) + }) + + it('should ignore invalid dimensions', () => { + // Given + const rows = [ + ['0x1111111111111111111111111111111111111111', 'tokenz', '10'], + ['0x2222222222222222222222222222222222222222', 'perzentage', '10'], + ['0x3333333333333333333333333333333333333333', 'units', '10'], + ['0x4444444444444444444444444444444444444444', '', '10'] + ] + const cb = jest.fn() + + // When + processReservedTokens(rows, cb) + + // Then + expect(cb).toHaveBeenCalledTimes(0) + }) +})