OSDN Git Service

Merge pull request #52 from Bytom/i18n
[bytom/bytom-electron.git] / src / features / transactions / components / New / NormalTransactionForm.jsx
1 import {
2   BaseNew,
3   FormSection,
4   TextField,
5   Autocomplete,
6   ObjectSelectorField,
7   AmountUnitField,
8   AmountInputMask,
9   ErrorBanner,
10   GasField
11 } from 'features/shared/components'
12 import {chainClient} from 'utility/environment'
13 import {reduxForm} from 'redux-form'
14 import React from 'react'
15 import styles from './New.scss'
16 import disableAutocomplete from 'utility/disableAutocomplete'
17 import { btmID } from 'utility/environment'
18 import actions from 'actions'
19 import ConfirmModal from './ConfirmModal/ConfirmModal'
20 import { balance , getAssetDecimal, normalTxActionBuilder} from '../../transactions'
21 import {withNamespaces} from 'react-i18next'
22
23 class NormalTxForm extends React.Component {
24   constructor(props) {
25     super(props)
26     this.connection = chainClient().connection
27     this.state = {
28       estimateGas:null,
29       counter: 1
30     }
31
32     this.submitWithValidation = this.submitWithValidation.bind(this)
33     this.disableSubmit = this.disableSubmit.bind(this)
34     this.addReceiverItem = this.addReceiverItem.bind(this)
35   }
36
37   disableSubmit() {
38     return !(this.state.estimateGas)
39   }
40
41   submitWithValidation(data) {
42     return new Promise((resolve, reject) => {
43       this.props.submitForm(Object.assign({}, data, {state: this.state, form: 'normalTx'}))
44         .then(() => {
45           this.props.closeModal()
46           this.props.destroyForm()
47         })
48         .catch((err) => {
49           if(err.message !== 'PasswordWrong'){
50             this.props.closeModal()
51           }
52           reject({_error: err})
53         })
54     })
55   }
56
57   confirmedTransaction(e, assetDecimal){
58     e.preventDefault()
59     this.props.showModal(
60       <ConfirmModal
61         cancel={this.props.closeModal}
62         onSubmit={this.submitWithValidation}
63         gas={this.state.estimateGas}
64         btmAmountUnit={this.props.btmAmountUnit}
65         assetDecimal={assetDecimal}
66         asset={this.props.asset}
67       />
68     )
69   }
70
71   addReceiverItem() {
72     const counter = this.state.counter
73     this.props.fields.receivers.addField({
74       id: counter
75     })
76     this.setState({
77       counter: counter+1,
78       estimateGas: null
79     })
80   }
81
82   removeReceiverItem(index) {
83     const receiver = this.props.fields.receivers
84     const promise = new Promise(function(resolve, reject) {
85       try {
86         receiver.removeField(index)
87       } catch (err) {
88         reject(err)
89       }
90       resolve()
91     })
92
93     promise.then(() =>  this.estimateNormalTransactionGas())
94   }
95
96   estimateNormalTransactionGas() {
97     const transaction = this.props.fields
98     const accountAlias = transaction.accountAlias.value
99     const accountId = transaction.accountId.value
100     const assetAlias = transaction.assetAlias.value
101     const assetId = transaction.assetId.value
102     const receivers = transaction.receivers
103     const addresses = receivers.map(x => x.address.value)
104     const amounts = receivers.map(x => Number(x.amount.value))
105
106     const {t, i18n} = this.props
107
108     const noAccount = !accountAlias && !accountId
109     const noAsset = !assetAlias && !assetId
110
111     if ( addresses.includes('') || amounts.includes(0)|| noAccount || noAsset) {
112       this.setState({estimateGas: null})
113       return
114     }
115
116     const actions = normalTxActionBuilder(transaction, Math.pow(10, 7), 'amount.value' )
117
118     const body = {actions, ttl: 1}
119     this.connection.request('/build-transaction', body).then(resp => {
120       if (resp.status === 'fail') {
121         this.setState({estimateGas: null})
122         const errorMsg =  resp.code && i18n.exists(`btmError.${resp.code}`) && t(`btmError.${resp.code}`) || resp.msg
123         this.props.showError(new Error(errorMsg))
124         return
125       }
126
127       return this.connection.request('/estimate-transaction-gas', {
128         transactionTemplate: resp.data
129       }).then(resp => {
130         if (resp.status === 'fail') {
131           this.setState({estimateGas: null})
132           const errorMsg =  resp.code && i18n.exists(`btmError.${resp.code}`) && t(`btmError.${resp.code}`) || resp.msg
133           this.props.showError(new Error(errorMsg))
134           return
135         }
136         this.setState({estimateGas: Math.ceil(resp.data.totalNeu/100000)*100000})
137       })
138     })
139   }
140
141   render() {
142     const {
143       fields: {accountId, accountAlias, assetId, assetAlias, receivers, gasLevel},
144       error,
145       submitting
146     } = this.props
147     const t = this.props.t;
148     [accountAlias, accountId, assetAlias, assetId].forEach(key => {
149       key.onBlur = this.estimateNormalTransactionGas.bind(this)
150     });
151     (receivers.map(receiver => receiver.amount)).forEach(amount =>{
152       amount.onBlur = this.estimateNormalTransactionGas.bind(this)
153     })
154
155     let submitLabel = t('transaction.new.submit')
156
157     const assetDecimal = getAssetDecimal(this.props.fields, this.props.asset) || 0
158
159     const showAvailableBalance = (accountAlias.value || accountId.value) &&
160       (assetAlias.value || assetId.value)
161
162     const availableBalance = balance(this.props.fields, assetDecimal, this.props.balances, this.props.btmAmountUnit)
163
164     const showBtmAmountUnit = (assetAlias.value === 'BTM' || assetId.value === btmID)
165
166     return (
167         <form
168           className={styles.container}
169           onSubmit={e => this.confirmedTransaction(e, assetDecimal)}
170           {...disableAutocomplete}
171         >
172           <div className={styles.borderBottom}>
173             <label className={styles.title}>{t('transaction.normal.from')}</label>
174             <div className={`${styles.mainBox} ${this.props.tutorialVisible? styles.tutorialItem: styles.item}`}>
175               <ObjectSelectorField
176                 key='account-selector-field'
177                 keyIndex='normaltx-account'
178                 title={t('form.account')}
179                 aliasField={Autocomplete.AccountAlias}
180                 fieldProps={{
181                   id: accountId,
182                   alias: accountAlias
183                 }}
184               />
185               <div>
186                 <ObjectSelectorField
187                   key='asset-selector-field'
188                   keyIndex='normaltx-asset'
189                   title={ t('form.asset')}
190                   aliasField={Autocomplete.AssetAlias}
191                   fieldProps={{
192                     id: assetId,
193                     alias: assetAlias
194                   }}
195                 />
196                 {showAvailableBalance && availableBalance &&
197                 <small className={styles.balanceHint}>{t('transaction.normal.availableBalance')} {availableBalance}</small>}
198               </div>
199             </div>
200
201             <label className={styles.title}>{t('transaction.normal.to')}</label>
202
203             <div className={styles.mainBox}>
204             {receivers.map((receiver, index) =>
205               <div
206                 className={this.props.tutorialVisible? styles.tutorialItem: styles.item}
207                 key={receiver.id.value}>
208                 <TextField title={t('form.address')} fieldProps={{
209                   ...receiver.address,
210                   onBlur: (e) => {
211                     receiver.address.onBlur(e)
212                     this.estimateNormalTransactionGas()
213                   },
214                 }}/>
215
216                 {!showBtmAmountUnit &&
217                 <AmountInputMask title={t('form.amount')} fieldProps={receiver.amount} decimal={assetDecimal}
218                 />}
219                 {showBtmAmountUnit &&
220                 <AmountUnitField title={t('form.amount')} fieldProps={receiver.amount}/>
221                 }
222
223                 {index===0 ?
224                   <a href='#' className={styles.receiverBtn} onClick={this.addReceiverItem}>+</a> :
225                   <a href='#' className={`${styles.receiverBtn} text-danger`} onClick={()=> this.removeReceiverItem(index)}>-</a>
226                 }
227
228               </div>
229             )}
230             </div>
231
232             <label className={styles.title}>{t('transaction.normal.selectFee')}</label>
233             <div className={styles.txFeeBox}>
234               <GasField
235                 gas={this.state.estimateGas}
236                 fieldProps={gasLevel}
237                 btmAmountUnit={this.props.btmAmountUnit}
238               />
239               <span className={styles.feeDescription}> {t('transaction.normal.feeDescription')}</span>
240             </div>
241           </div>
242
243           <FormSection className={styles.submitSection}>
244             {error && error.message !== 'PasswordWrong' &&
245             <ErrorBanner
246               title={t('form.errorTitle')}
247               error={error} />}
248
249             <div className={styles.submit}>
250               <button type='submit' className='btn btn-primary'
251                       disabled={submitting || this.disableSubmit()}>
252                 {submitLabel}
253               </button>
254             </div>
255           </FormSection>
256         </form>
257     )
258   }
259 }
260
261 const validate = (values, props) => {
262   const errors = {gas: {}}
263   const t = props.t
264
265   // Numerical
266   if (values.amount && !/^\d+(\.\d+)?$/i.test(values.amount)) {
267     errors.amount = ( t('errorMessage.amountError') )
268   }
269   return errors
270 }
271
272 const asyncValidate = (values, dispatch, props) => {
273   const errors = []
274   const promises = []
275
276   values.receivers.forEach((receiver, idx) => {
277     const address = values.receivers[idx].address
278     if ( !address || address.length === 0)
279       promises.push(Promise.resolve())
280     else{
281       promises.push(
282         chainClient().accounts.validateAddresses(address)
283           .then(
284             (resp) => {
285               if (!resp.data.valid) {
286                 errors[idx] = {address: props.t('errorMessage.addressError')}
287               }
288               return {}
289             }
290           ))
291     }
292   })
293
294   return Promise.all(promises).then(() => {
295     if (errors.length > 0) throw {
296       receivers: errors
297     }
298     return {}
299   })
300 }
301
302 const mapDispatchToProps = (dispatch) => ({
303   showError: err => dispatch({type: 'ERROR', payload: err}),
304   closeModal: () => dispatch(actions.app.hideModal),
305   showModal: (body) => dispatch(actions.app.showModal(
306     body,
307     actions.app.hideModal,
308     null,
309     {
310       dialog: true,
311       noCloseBtn: true
312     }
313   )),
314   ...BaseNew.mapDispatchToProps('transaction')(dispatch)
315 })
316
317 export default withNamespaces('translations') (BaseNew.connect(
318   BaseNew.mapStateToProps('transaction'),
319   mapDispatchToProps,
320   reduxForm({
321     form: 'NormalTransactionForm',
322     fields: [
323       'accountAlias',
324       'accountId',
325       'assetAlias',
326       'assetId',
327       'receivers[].id',
328       'receivers[].amount',
329       'receivers[].address',
330       'gasLevel',
331     ],
332     asyncValidate,
333     asyncBlurFields: ['receivers[].address'],
334     validate,
335     touchOnChange: true,
336     initialValues: {
337       gasLevel: '1',
338       receivers:[{
339         id: 0,
340         amount:'',
341         address:''
342       }]
343     },
344   })(NormalTxForm)
345 ))