OSDN Git Service

c1eac345489e66943a68824d0dbef410b678b243
[bytom/bytom-electron.git] / src / features / transactions / components / New / New.jsx
1 import {
2   BaseNew,
3   FormContainer,
4   FormSection,
5   FieldLabel,
6   TextField,
7   Autocomplete,
8   ObjectSelectorField,
9   AmountUnitField,
10   AmountInputMask
11 } from 'features/shared/components'
12 import {DropdownButton, MenuItem} from 'react-bootstrap'
13 import {reduxForm} from 'redux-form'
14 import ActionItem from './FormActionItem'
15 import React from 'react'
16 import styles from './New.scss'
17 import actions from 'actions'
18 import { normalizeBTMAmountUnit, converIntToDec } from 'utility/buildInOutDisplay'
19
20 const rangeOptions = [
21   {
22     label: 'Standard',
23     label_zh: '标准',
24     value: '20000000'
25   },
26   {
27     label: 'Fast',
28     label_zh: '快速',
29     value: '25000000'
30   },
31   {
32     label: 'Customize',
33     label_zh: '自定义',
34     value: ''
35   }
36 ]
37
38 const btmID = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
39
40 class Form extends React.Component {
41   constructor(props) {
42     super(props)
43     this.state = {
44       showDropdown: false,
45       showAdvanced: false
46     }
47
48     this.submitWithValidation = this.submitWithValidation.bind(this)
49     this.addActionItem = this.addActionItem.bind(this)
50     this.removeActionItem = this.removeActionItem.bind(this)
51     this.toggleDropwdown = this.toggleDropwdown.bind(this)
52     this.closeDropdown = this.closeDropdown.bind(this)
53     this.disableSubmit = this.disableSubmit.bind(this)
54   }
55
56   componentDidMount() {
57     if (!this.props.autocompleteIsLoaded) {
58       this.props.fetchAll().then(() => {
59         this.props.didLoadAutocomplete()
60       })
61     }
62     if (!this.props.autocompleteIsAssetLoaded) {
63       this.props.fetchAssetAll().then(() => {
64         this.props.didLoadAssetAutocomplete()
65       })
66     }
67
68     this.props.fields.normalTransaction.gas.type.onChange(rangeOptions[0].label)
69     this.props.fields.normalTransaction.gas.price.onChange(rangeOptions[0].value)
70   }
71
72   balanceAmount(normalTransaction, assetdecimal) {
73     let balances = this.props.balances
74     let filteredBalances = balances
75     if (normalTransaction.accountAlias.value) {
76       filteredBalances = filteredBalances.filter(balance => balance.accountAlias === normalTransaction.accountAlias.value)
77     }
78     if (normalTransaction.accountId.value) {
79       filteredBalances = filteredBalances.filter(balance => balance.accountId === normalTransaction.accountId.value)
80     }
81     if (normalTransaction.assetAlias.value) {
82       filteredBalances = filteredBalances.filter(balance => balance.assetAlias === normalTransaction.assetAlias.value)
83     }
84     if (normalTransaction.assetId.value) {
85       filteredBalances = filteredBalances.filter(balance => balance.assetId === normalTransaction.assetId.value)
86     }
87
88     if(filteredBalances.length === 1){
89       if (filteredBalances[0].assetId === btmID){
90         return normalizeBTMAmountUnit(filteredBalances[0].assetId, filteredBalances[0].amount, this.props.btmAmountUnit)
91       }else if( assetdecimal ){
92         return converIntToDec(filteredBalances[0].amount, assetdecimal)
93       }else{
94         return filteredBalances[0].amount
95       }
96     }else {
97       return null
98     }
99   }
100
101   assetDecimal(normalTransaction) {
102     let asset = this.props.asset
103     let filteredAsset = asset
104     if (normalTransaction.assetAlias.value) {
105       filteredAsset = filteredAsset.filter(asset => asset.alias === normalTransaction.assetAlias.value)
106     }
107     if (normalTransaction.assetId.value) {
108       filteredAsset = filteredAsset.filter(asset => asset.id === normalTransaction.assetId.value)
109     }
110
111     return (filteredAsset.length === 1 && filteredAsset[0].definition && filteredAsset[0].id !== btmID ) ? filteredAsset[0].definition.decimals : null
112   }
113
114   toggleDropwdown() {
115     this.setState({showDropdown: !this.state.showDropdown})
116   }
117
118   closeDropdown() {
119     this.setState({showDropdown: false})
120   }
121
122   addActionItem(type) {
123     this.props.fields.actions.addField({
124       type: type,
125       referenceData: '{\n\t\n}'
126     })
127     this.closeDropdown()
128   }
129
130   disableSubmit(actions, normalTransaction) {
131     if (this.state.showAdvanceTx) {
132       return actions.length == 0 && !this.state.showAdvanced
133     }
134
135     const hasValue = target => {
136       return !!(target && target.value)
137     }
138
139     return !((hasValue(normalTransaction.accountId) || hasValue(normalTransaction.accountAlias)) &&
140       (hasValue(normalTransaction.assetId) || hasValue(normalTransaction.assetAlias)) &&
141       hasValue(normalTransaction.address) && (hasValue(normalTransaction.amount)))
142   }
143
144   removeActionItem(index) {
145     this.props.fields.actions.removeField(index)
146   }
147
148   emptyActions(actions) {
149     if (actions.length != 0) {
150       actions.map(() => this.removeActionItem(0))
151     }
152   }
153
154   submitWithValidation(data) {
155     return new Promise((resolve, reject) => {
156       this.props.submitForm(Object.assign({}, data, {state: this.state}))
157         .catch((err) => {
158           const response = {}
159
160           if (err.data) {
161             response.actions = []
162
163             err.data.forEach((error) => {
164               response.actions[error.data.actionIndex] = {type: error}
165             })
166           }
167
168           response['_error'] = err
169           return reject(response)
170         })
171     })
172   }
173
174
175   render() {
176     const {
177       fields: {baseTransaction, actions, submitAction, password, normalTransaction},
178       error,
179       handleSubmit,
180       submitting
181     } = this.props
182     const lang = this.props.lang
183
184     let submitLabel = lang === 'zh' ? '提交交易' : 'Submit transaction'
185     const hasBaseTransaction = ((baseTransaction.value || '').trim()).length > 0
186     if (submitAction.value == 'generate' && !hasBaseTransaction) {
187       submitLabel = lang === 'zh' ? '生成交易JSON' : 'Generate transaction JSON'
188     }
189
190     const gasOnChange = event => {
191       normalTransaction.gas.type.onChange(event)
192
193       const range = rangeOptions.find(item => item.label === event.target.value)
194       normalTransaction.gas.price.onChange(range.value)
195     }
196     const assetDecimal = this.assetDecimal(normalTransaction)
197
198     const showAvailableBalance = (normalTransaction.accountAlias.value || normalTransaction.accountId.value) &&
199       (normalTransaction.assetAlias.value || normalTransaction.assetId.value)
200     const availableBalance = this.balanceAmount(normalTransaction, assetDecimal)
201
202     const showBtmAmountUnit = (normalTransaction.assetAlias.value === 'BTM' ||
203       normalTransaction.assetId.value === 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
204
205     return (
206       <FormContainer
207         error={error}
208         label={lang === 'zh' ? '新建交易' : 'New transaction'}
209         submitLabel={submitLabel}
210         onSubmit={handleSubmit(this.submitWithValidation)}
211         showSubmitIndicator={true}
212         submitting={submitting}
213         disabled={this.disableSubmit(actions, normalTransaction)}>
214
215         <div className={`btn-group ${styles.btnGroup}`} role='group'>
216           <button
217             className={`btn btn-default ${this.state.showAdvanceTx ? null : 'active'}`}
218             onClick={(e) => {
219               e.preventDefault()
220               this.emptyActions(actions)
221               this.setState({showAdvanceTx: false})
222             }}>
223             {lang === 'zh' ? '简单交易' : 'Normal'}
224           </button>
225           <button
226             className={`btn btn-default ${this.state.showAdvanceTx ? 'active' : null}`}
227             onClick={(e) => {
228               e.preventDefault()
229               this.setState({showAdvanceTx: true})
230             }}>
231             {lang === 'zh' ? '高级交易' : 'Advanced'}
232           </button>
233         </div>
234
235         {!this.state.showAdvanceTx && <FormSection title={lang === 'zh' ? '简单交易' : 'Normal Trasaction'}>
236           <label className={styles.title}>{lang === 'zh' ? '从' : 'From'}</label>
237           <div className={styles.main}>
238             <ObjectSelectorField
239               key='account-selector-field'
240               lang={lang}
241               title={lang === 'zh' ? '账户' : 'Account'}
242               aliasField={Autocomplete.AccountAlias}
243               fieldProps={{
244                 id: normalTransaction.accountId,
245                 alias: normalTransaction.accountAlias
246               }}
247             />
248             <ObjectSelectorField
249               key='asset-selector-field'
250               lang={lang}
251               title={lang === 'zh' ? '资产' : 'Asset'}
252               aliasField={Autocomplete.AssetAlias}
253               fieldProps={{
254                 id: normalTransaction.assetId,
255                 alias: normalTransaction.assetAlias
256               }}
257             />
258             {showAvailableBalance && availableBalance &&
259             <small className={styles.balanceHint}>{availableBalance} {lang === 'zh' ? '可用' : 'available'} </small>}
260           </div>
261
262           <label className={styles.title}>{lang === 'zh' ? '至' : 'To'}</label>
263           <div className={styles.main}>
264             <TextField title={lang === 'zh' ? '地址' : 'Address'} fieldProps={normalTransaction.address}/>
265             {!showBtmAmountUnit && !assetDecimal &&
266             <TextField title={lang === 'zh' ? '数量' : 'Amount'} fieldProps={normalTransaction.amount}
267             />}
268             {!showBtmAmountUnit && assetDecimal &&
269             <AmountInputMask title={lang === 'zh' ? '数量' : 'Amount'} fieldProps={normalTransaction.amount} decimal={assetDecimal}
270             />}
271             {showBtmAmountUnit &&
272             <AmountUnitField title={lang === 'zh' ? '数量' : 'Amount'} fieldProps={normalTransaction.amount}/>
273             }
274           </div>
275
276           <label className={styles.title}>Gas</label>
277           <table className={styles.optionsBtnContianer}>
278             {rangeOptions.map((option) =>
279               <tr className={styles.optionsBtn}>
280                 <td className={styles.optionsLabel}>
281                   <label>
282                     <input type='radio'
283                            {...normalTransaction.gas.type}
284                            onChange={gasOnChange}
285                            value={option.label}
286                            checked={option.label == normalTransaction.gas.type.value}
287                     />
288                     {lang === 'zh' ? option.label_zh : option.label}
289                   </label>
290                 </td>
291                 <td>
292                   {option.label == normalTransaction.gas.type.value && option.label !== 'Customize'
293                   && normalizeBTMAmountUnit(btmID, option.value, this.props.btmAmountUnit)}
294                   {
295                     option.label === 'Customize' && normalTransaction.gas.type.value === 'Customize' &&
296                     <div>
297                       <AmountUnitField
298                         autoFocus={true}
299                         fieldProps={normalTransaction.gas.price}
300                         placeholder='Enter gas'/>
301                     </div>
302                   }
303                 </td>
304               </tr>
305             )}
306           </table>
307
308           <label className={styles.title}>{lang === 'zh' ? '密码' : 'Password'}</label>
309           <TextField placeholder={lang === 'zh' ? '请输入密码' : 'Please enter the assword'} fieldProps={password}
310                      autoFocus={false} type={'password'}/>
311         </FormSection>}
312
313         {this.state.showAdvanceTx && <FormSection title='Actions'>
314           {actions.map((action, index) =>
315             <ActionItem
316               key={index}
317               index={index}
318               fieldProps={action}
319               accounts={this.props.accounts}
320               assets={this.props.assets}
321               remove={this.removeActionItem}
322               lang={lang}
323               decimal={this.assetDecimal(action)}
324             />)}
325
326           <div className={`btn-group ${styles.addActionContainer} ${this.state.showDropdown && 'open'}`}>
327             <DropdownButton
328               className={`btn btn-default ${styles.addAction}`}
329               id='input-dropdown-addon'
330               title='+ Add action'
331               onSelect={this.addActionItem}
332             >
333               <MenuItem eventKey='issue'>Issue</MenuItem>
334               <MenuItem eventKey='spend_account'>Spend from account</MenuItem>
335               <MenuItem eventKey='control_address'>Control with address</MenuItem>
336               <MenuItem eventKey='retire'>Retire</MenuItem>
337             </DropdownButton>
338           </div>
339         </FormSection>}
340
341         {this.state.showAdvanceTx && !this.state.showAdvanced &&
342         <FormSection>
343           <a href='#'
344              className={styles.showAdvanced}
345              onClick={(e) => {
346                e.preventDefault()
347                this.setState({showAdvanced: true})
348              }}
349           >
350             {lang === 'zh' ? '显示高级选项' : 'Show advanced options'}
351           </a>
352         </FormSection>
353         }
354
355         {this.state.showAdvanceTx && this.state.showAdvanced &&
356         <FormSection title={lang === 'zh' ? '高级选项' : 'Advanced Options'}>
357           <div>
358             <TextField
359               title={lang === 'zh' ? '带签名交易' : 'To sign transaction'}
360               placeholder={lang === 'zh' ? '在这里复制交易 HEX ...' : 'Paste transaction hex here...'}
361               fieldProps={baseTransaction}
362               autoFocus={true}/>
363
364             <FieldLabel>{lang === 'zh' ? '交易构建类型' : 'Transaction Build Type'}</FieldLabel>
365             <table className={styles.submitTable}>
366               <tbody>
367               <tr>
368                 <td><input id='submit_action_submit' type='radio' {...submitAction} value='submit'
369                            checked={submitAction.value == 'submit'}/></td>
370                 <td>
371                   <label
372                     htmlFor='submit_action_submit'>{lang === 'zh' ? '向区块链提交交易' : 'Submit transaction to blockchain'}</label>
373                   <br/>
374                   <label htmlFor='submit_action_submit' className={styles.submitDescription}>
375                     {lang === 'zh' ? '此次交易将通过密钥签名然后提交到区块链。' :
376                       'This transaction will be signed by the MockHSM and submitted to the blockchain.'}
377                   </label>
378                 </td>
379               </tr>
380               <tr>
381                 <td><input id='submit_action_generate' type='radio' {...submitAction} value='generate'
382                            checked={submitAction.value == 'generate'}/></td>
383                 <td>
384                   <label htmlFor='submit_action_generate'>{lang === 'zh' ? '需要更多签名' : 'Need more signature'}</label>
385                   <br/>
386                   <label htmlFor='submit_action_generate' className={styles.submitDescription}>
387                     {lang === 'zh' ? '这些actions将通过密钥签名然后作为一个交易 JSON 字符串返回。 作为多签交易的输入,这个JSON字符串需要更多的签名数据。' :
388                       'These actions will be signed by the Key and returned as a transaction JSON string, ' +
389                       'which should be used to sign transaction in a multi-sign spend.'}
390                   </label>
391                 </td>
392               </tr>
393               </tbody>
394             </table>
395           </div>
396         </FormSection>}
397
398         {
399           this.state.showAdvanceTx && (actions.length > 0 || this.state.showAdvanced) && <FormSection>
400             <label className={styles.title}>{lang === 'zh' ? '密码' : 'Password'}</label>
401             <TextField placeholder={lang === 'zh' ? '请输入密码' : 'Please enter the assword'} fieldProps={password}
402                        autoFocus={false} type={'password'}/>
403           </FormSection>
404         }
405       </FormContainer>
406     )
407   }
408 }
409
410 const validate = values => {
411   const errors = {actions: {}, normalTransaction: {gas: {}}}
412
413   // Base transaction
414   let baseTx = (values.baseTransaction || '').trim()
415   try {
416     JSON.parse(baseTx)
417   } catch (e) {
418     if (baseTx && e) {
419       errors.baseTransaction = 'To sign transaction must be a JSON string.'
420     }
421   }
422
423   // Actions
424   let numError
425   values.actions.forEach((action, index) => {
426     numError = (!/^\d+(\.\d+)?$/i.test(values.actions[index].amount))
427     if (numError) {
428       errors.actions[index] = {...errors.actions[index], amount: 'Invalid amount type'}
429     }
430   })
431
432   // Numerical
433   let normalTx = values.normalTransaction || ''
434   if (normalTx.amount && !/^\d+(\.\d+)?$/i.test(normalTx.amount)) {
435     errors.normalTransaction.amount = 'Invalid amount type'
436   }
437   return errors
438 }
439
440 export default BaseNew.connect(
441   (state) => {
442     let balances = []
443     for (let key in state.balance.items) {
444       balances.push(state.balance.items[key])
445     }
446
447     return {
448       autocompleteIsLoaded: state.balance.autocompleteIsLoaded,
449       autocompleteIsAssetLoaded: state.balance.autocompleteIsLoaded,
450       lang: state.core.lang,
451       btmAmountUnit: state.core.btmAmountUnit,
452       balances,
453       asset: Object.keys(state.asset.items).map(k => state.asset.items[k]),
454       ...BaseNew.mapStateToProps('transaction')(state)
455     }
456   },
457   (dispatch) => ({
458     didLoadAutocomplete: () => dispatch(actions.balance.didLoadAutocomplete),
459     fetchAll: (cb) => dispatch(actions.balance.fetchAll(cb)),
460     didLoadAssetAutocomplete: () => dispatch(actions.asset.didLoadAutocomplete),
461     fetchAssetAll: (cb) => dispatch(actions.asset.fetchAll(cb)),
462     ...BaseNew.mapDispatchToProps('transaction')(dispatch)
463   }),
464   reduxForm({
465     form: 'NewTransactionForm',
466     fields: [
467       'baseTransaction',
468       'actions[].accountId',
469       'actions[].accountAlias',
470       'actions[].assetId',
471       'actions[].assetAlias',
472       'actions[].amount',
473       'actions[].receiver',
474       'actions[].outputId',
475       'actions[].referenceData',
476       'actions[].type',
477       'actions[].address',
478       'actions[].password',
479       'normalTransaction.accountAlias',
480       'normalTransaction.accountId',
481       'normalTransaction.amount',
482       'normalTransaction.assetAlias',
483       'normalTransaction.assetId',
484       'normalTransaction.gas',
485       'normalTransaction.gas.type',
486       'normalTransaction.gas.price',
487       'normalTransaction.address',
488       'submitAction',
489       'password'
490     ],
491     validate,
492     touchOnChange: true,
493     initialValues: {
494       submitAction: 'submit',
495     },
496   }
497   )(Form)
498 )
499
500