/
UpdateTokenPanel.js
382 lines (345 loc) · 9.17 KB
/
UpdateTokenPanel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
import React, { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import BN from 'bn.js'
import {
Button,
Field,
formatTokenAmount,
GU,
Info,
SidePanel,
useSidePanelFocusOnReady,
} from '@aragon/ui'
import { isAddress } from '../../web3-utils'
import { fromDecimals, toDecimals, splitDecimalNumber } from '../../utils'
import LocalIdentitiesAutoComplete from '../LocalIdentitiesAutoComplete/LocalIdentitiesAutoComplete'
import AmountInput from '../AmountInput'
// Any more and the number input field starts to put numbers in scientific notation
const MAX_INPUT_DECIMAL_BASE = 6
function UpdateTokenPanel({
getHolderBalance,
holderAddress,
maxAccountTokens,
mode,
onClose,
onTransitionEnd,
onUpdateTokens,
opened,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
}) {
return (
<SidePanel
title={mode === 'assign' ? 'Add tokens' : 'Remove tokens'}
opened={opened}
onClose={onClose}
onTransitionEnd={onTransitionEnd}
>
<TokenPanelContent
getHolderBalance={getHolderBalance}
holderAddress={holderAddress}
maxAccountTokens={maxAccountTokens}
mode={mode}
onUpdateTokens={onUpdateTokens}
opened={opened}
tokenDecimals={tokenDecimals}
tokenDecimalsBase={tokenDecimalsBase}
tokenSymbol={tokenSymbol}
/>
</SidePanel>
)
}
function usePanelForm({
getHolderBalance,
initialHolder,
maxAccountTokens,
mode,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
}) {
const [holderField, setHolderField] = useState({
error: null,
value: initialHolder,
warning: null,
})
const [amountField, setAmountField] = useState({
error: null,
max: '',
value: '',
warning: null,
})
const holderBalance =
holderField.value && !holderField.error
? getHolderBalance(holderField.value)
: new BN('0')
const errorMessage = holderField.error || amountField.error
const warningMessage = holderField.warning || amountField.warning
const submitDisabled = Boolean(
errorMessage ||
warningMessage ||
!holderField.value ||
!amountField.value ||
amountField.max === '0' ||
amountField.value === '0'
)
const getMaxAmountFromBalance = useCallback(
balance => (mode === 'assign' ? maxAccountTokens.sub(balance) : balance),
[mode, maxAccountTokens]
)
const updateHolder = useCallback(
value => {
const maxAmount = getMaxAmountFromBalance(getHolderBalance(value.trim()))
const maxAmountLabel = formatTokenAmount(maxAmount, tokenDecimals)
setHolderField(holderField => ({
...holderField,
error: null,
value,
warning:
maxAmount.isZero() &&
(mode === 'assign'
? `The maximum amount of tokens that can be assigned
(${maxAmountLabel} ${tokenSymbol}) has already been reached.`
: 'This account doesn’t have any tokens to remove.'),
}))
setAmountField(amountField => ({
...amountField,
max: formatTokenAmount(maxAmount, tokenDecimals),
}))
},
[
getHolderBalance,
getMaxAmountFromBalance,
mode,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
]
)
const updateAmount = useCallback(
value => {
const formattedAmount = toDecimals(value.trim(), tokenDecimals)
if (formattedAmount === '0') {
// Given value is smaller than the accepted decimal base (e.g. gave 0.5 to a token base of 1)
setAmountField(amountField => ({
...amountField,
value,
warning: `You are trying to ${
mode === 'assign' ? 'assign' : 'remove'
} an amount that is smaller than the minimum amount of tokens possible.`,
}))
return
}
const decimals = splitDecimalNumber(value.trim())[1]
if (decimals.length > tokenDecimals) {
// Given value has more precision than we expected
setAmountField(amountField => ({
...amountField,
value,
warning: `You are trying to ${
mode === 'assign' ? 'assign' : 'remove'
} an amount that includes more decimals than the token allows.`,
}))
return
}
const amount = new BN(formattedAmount)
const maxAmount = getMaxAmountFromBalance(holderBalance)
setAmountField(amountField => ({
...amountField,
value,
warning: amount.gt(maxAmount)
? `You are trying to ${
mode === 'assign' ? 'assign' : 'remove'
} an amount that is greater than the
maximum amount of tokens that can be ${
mode === 'assign' ? 'assigned' : 'removed'
} (${formatTokenAmount(maxAmount, tokenDecimals, {
symbol: tokenSymbol,
})} ).`
: null,
}))
},
[
mode,
holderBalance,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
getMaxAmountFromBalance,
]
)
const validateFields = useCallback(() => {
const holderAddress = holderField.value.trim()
const holderError = isAddress(holderAddress)
? null
: mode === 'assign'
? 'The recipient must be a valid Ethereum address.'
: 'The account must be a valid Ethereum address.'
if (holderError) {
setHolderField({ ...holderField, error: holderError })
return null
}
return {
holder: holderField.value.trim(),
amount: toDecimals(amountField.value.trim(), tokenDecimals),
}
}, [mode, holderField, amountField, tokenDecimals])
useEffect(() => {
updateHolder(initialHolder)
}, [initialHolder, updateHolder])
return {
amountField,
errorMessage,
holderField,
submitDisabled,
updateAmount,
updateHolder,
validateFields,
warningMessage,
holderBalance,
}
}
function TokenPanelContent({
getHolderBalance,
holderAddress,
maxAccountTokens,
mode,
onUpdateTokens,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
}) {
const holderInputRef = useSidePanelFocusOnReady()
const amountInputRef = useSidePanelFocusOnReady()
const {
amountField,
errorMessage,
holderField,
submitDisabled,
updateAmount,
updateHolder,
validateFields,
warningMessage,
} = usePanelForm({
getHolderBalance,
initialHolder: holderAddress,
maxAccountTokens,
mode,
onUpdateTokens,
tokenDecimals,
tokenDecimalsBase,
tokenSymbol,
})
const tokenStep = fromDecimals(
'1',
Math.min(MAX_INPUT_DECIMAL_BASE, tokenDecimals)
)
const handleAmountChange = useCallback(
event => updateAmount(event.target.value),
[updateAmount]
)
const handleSubmit = useCallback(
event => {
event.preventDefault()
const fieldsData = validateFields()
if (!fieldsData) {
return
}
onUpdateTokens({
amount: fieldsData.amount,
holder: fieldsData.holder,
mode,
})
},
[mode, validateFields, onUpdateTokens]
)
return (
<form
css={`
margin-top: ${3 * GU}px;
`}
onSubmit={handleSubmit}
>
<Info
title="Action"
css={`
margin-bottom: ${3 * GU}px;
`}
>
{mode === 'assign'
? 'This action will create tokens and transfer them to the recipient below.'
: 'This action will remove tokens from the account below.'}
</Info>
<Field
label={
mode === 'assign'
? 'Recipient (must be a valid Ethereum address)'
: 'Account (must be a valid Ethereum address)'
}
>
<LocalIdentitiesAutoComplete
ref={holderAddress ? undefined : holderInputRef}
value={holderField.value}
onChange={updateHolder}
wide
required
/>
</Field>
<Field
label={
mode === 'assign'
? 'Number of tokens to add'
: 'Number of tokens to remove'
}
>
<AmountInput
ref={holderAddress ? amountInputRef : undefined}
onChange={handleAmountChange}
onMaxClick={() => updateAmount(amountField.max)}
step={tokenStep}
value={amountField.value}
required
showMax={mode === 'remove'}
wide
/>
</Field>
<Button mode="strong" type="submit" disabled={submitDisabled} wide>
{mode === 'assign' ? 'Add tokens' : 'Remove tokens'}
</Button>
<div
css={`
margin-top: ${2 * GU}px;
`}
>
{errorMessage && <Message mode="error">{errorMessage}</Message>}
{warningMessage && <Message mode="warning">{warningMessage}</Message>}
</div>
</form>
)
}
TokenPanelContent.propTypes = {
onUpdateTokens: PropTypes.func,
mode: PropTypes.string,
holderAddress: PropTypes.string,
}
TokenPanelContent.defaultProps = {
onUpdateTokens: () => {},
holderAddress: '',
}
function Message({ children, mode, title }) {
return (
<div
css={`
& + & {
margin-top: ${2 * GU}px;
}
`}
>
<Info mode={mode} title={title}>
{children}
</Info>
</div>
)
}
export default UpdateTokenPanel