import {parseNonblankJSON} from 'utility/string'
import {push} from 'react-router-redux'
import {baseCreateActions, baseListActions} from 'features/shared/actions'
-import { normalTxActionBuilder, issueAssetTxActionBuilder } from './transactions'
+import { voteTxActionBuilder, normalTxActionBuilder, issueAssetTxActionBuilder } from './transactions'
const type = 'transaction'
builder.actions = normalTxActionBuilder(formParams, Number(gasPrice), 'amount')
}
+ if (formParams.form === 'voteTx') {
+ const gasPrice = formParams.state.estimateGas * Number(formParams.gasLevel)
+ builder.actions = voteTxActionBuilder(formParams, Number(gasPrice), formParams.state.address)
+ }
+
if (formParams.form === 'issueAssetTx') {
const gasPrice = formParams.state.estimateGas * Number(formParams.gasLevel)
builder.actions = issueAssetTxActionBuilder(formParams, Number(gasPrice), 'amount')
for (let i in builder.actions) {
let a = builder.actions[i]
- const intFields = ['amount', 'position']
+ delete a.ID
+ const intFields = ['amount', 'position','sourcePos']
intFields.forEach(key => {
const value = a[key]
if (value) {
throw new Error(`Action ${parseInt(i) + 1} receiver should be valid JSON.`)
}
}
-
return builder
}
}
// normal transactions
- if( formParams.form === 'normalTx'){
+ if( formParams.form === 'normalTx' || formParams.form === 'voteTx'){
const accountId = formParams.accountId
const accountAlias = formParams.accountAlias
}
}
+const getAddresses =(params) => {
+ return new Promise((resolve, reject) => {
+ const body = Object.assign({from:0, count:1}, params)
+ chainClient().accounts.listAddresses(body)
+ .then((resp) =>{
+ const result = resp.data
+ if(result.length > 0){
+ resolve(result[0].address)
+ }else{
+ chainClient().accounts.createAddress(params)
+ .then((ret) =>{
+ resolve(ret.data.address)
+ }).catch((err) => reject( err))
+ }
+ })
+ .catch((err) => reject(err))
+ })
+}
+
+const estimateGas =(template) => {
+ return chainClient().transactions.estimateGas(template)
+}
+
+const buildTransaction = (builderBlock) =>{
+ const builderFunction = ( builder ) => {
+ const processed = preprocessTransaction(builderBlock)
+
+ builder.actions = processed.actions
+ builder.ttl = builderBlock.ttl
+ if (processed.baseTransaction) {
+ builder.baseTransaction = processed.baseTransaction
+ }
+
+ }
+ return chainClient().transactions.build(builderFunction)
+}
+
export default {
...list,
...form,
decode,
+ getAddresses,
+ estimateGas,
+ buildTransaction
}
title='+ Add action'
onSelect={this.addActionItem}
>
- <MenuItem eventKey='issue'>Issue</MenuItem>
<MenuItem eventKey='spend_account'>Spend from account</MenuItem>
<MenuItem eventKey='control_address'>Control with address</MenuItem>
- <MenuItem eventKey='retire'>Retire</MenuItem>
+ <MenuItem eventKey='vote_output'>Vote Output</MenuItem>
+ <MenuItem eventKey='unvote'>Unvote</MenuItem>
+ <MenuItem eventKey='cross_chain_in'>Cross Chain In</MenuItem>
+ <MenuItem eventKey='cross_chain_out'>Cross Chain Out</MenuItem>
</DropdownButton>
</div>
</FormSection>
'actions[].outputId',
'actions[].type',
'actions[].address',
+ 'actions[].vote',
+ 'actions[].sourceId',
+ 'actions[].sourcePos',
'actions[].password',
'submitAction',
'password'
import { btmID } from 'utility/environment'
import {withNamespaces} from 'react-i18next'
-const ISSUE_KEY = 'issue'
+const CROSS_CHAIN_OUT_KEY = 'cross_chain_out'
+const CROSS_CHAIN_IN_KEY = 'cross_chain_in'
+const VOTE_OUTPUT_KEY = 'vote_output'
+const UNVOTE_KEY = 'unvote'
const SPEND_ACCOUNT_KEY = 'spend_account'
const SPEND_UNSPENT_KEY = 'spend_account_unspent_output'
const CONTROL_RECEIVER_KEY = 'control_receiver'
const TRANSACTION_REFERENCE_DATA = 'set_transaction_reference_data'
const actionLabels = {
- [ISSUE_KEY]: 'Issue',
+ [CROSS_CHAIN_OUT_KEY]: 'Cross Chain Out',
+ [CROSS_CHAIN_IN_KEY]: 'Cross Chain In',
+ [VOTE_OUTPUT_KEY]: 'Vote Output',
+ [UNVOTE_KEY]: 'Unvote',
[SPEND_ACCOUNT_KEY]: 'Spend from account',
[SPEND_UNSPENT_KEY]: 'Spend unspent output',
[CONTROL_RECEIVER_KEY]: 'Control with receiver',
}
const visibleFields = {
- [ISSUE_KEY]: {asset: true, amount: true, password: true},
+ [CROSS_CHAIN_OUT_KEY]: {asset: true, amount: true, address:true},
+ [CROSS_CHAIN_IN_KEY]: {asset: true, amount: true, sourceId:true, sourcePos:true},
+ [VOTE_OUTPUT_KEY]: {asset: true, amount: true, address:true, vote:true},
+ [UNVOTE_KEY]: {account:true, amount: true, asset: true, vote:true},
[SPEND_ACCOUNT_KEY]: {asset: true, account: true, amount: true, password: true},
[SPEND_UNSPENT_KEY]: {outputId: true, password: true},
[CONTROL_RECEIVER_KEY]: {asset: true, receiver: true, amount: true},
assetAlias,
password,
amount,
+ vote,
+ sourceId,
+ sourcePos,
referenceData } = this.props.fieldProps
const visible = visibleFields[type.value] || {}
<JsonField title='Receiver' fieldProps={receiver} />}
{visible.address && <TextField title={ t('form.address' )} fieldProps={address} />}
+ {visible.vote && <TextField title={ t('form.vote' )} fieldProps={vote} />}
+ {visible.sourceId && <TextField title={ t('form.sourceId' )} fieldProps={sourceId} />}
+ {visible.sourcePos && <TextField title={ t('form.sourcePos' )} fieldProps={sourcePos} />}
{visible.outputId &&
<TextField title='Output ID' fieldProps={outputId} />}
import Tutorial from 'features/tutorial/components/Tutorial'
import NormalTxForm from './NormalTransactionForm'
import AdvancedTxForm from './AdvancedTransactionForm'
-import IssueAssets from './IssueAssets'
+import Vote from './Vote'
import { withRouter } from 'react-router'
import {getValues} from 'redux-form'
import {withNamespaces} from 'react-i18next'
return !(this.props.advform['actions'].length > 0 ||
this.props.advform['signTransaction'] ||
this.props.advform['password'])
- }else if(this.props.issueAssetSelected){
+ }else if(this.props.voteSelected){
+ const array = [
+ 'accountAlias',
+ 'accountId',
+ 'amount',
+ 'nodePubkey'
+ ]
+
+ for (let k in array){
+ if(this.props.voteform[array[k]]){
+ return false
+ }
+ }
return true
}
}
{t('transaction.new.advanced')}
</button>
<button
- className={`btn btn-default ${this.props.issueAssetSelected && 'active'}`}
- onClick={(e) => this.showForm(e, 'issueAsset')}>
- {t('transaction.issue.issueAsset')}
+ className={`btn btn-default ${this.props.voteSelected && 'active'}`}
+ onClick={(e) => this.showForm(e, 'vote')}>
+ {t('transaction.new.vote')}
</button>
</div>
</div>
handleKeyDown={this.handleKeyDown}
/>}
- {this.props.issueAssetSelected &&
- <IssueAssets
+ {this.props.voteSelected &&
+ <Vote
handleKeyDown={this.handleKeyDown}
{...this.props}
/>}
asset: Object.keys(state.asset.items).map(k => state.asset.items[k]),
normalform: getValues(state.form.NormalTransactionForm),
advform: getValues(state.form.AdvancedTransactionForm),
+ voteform: getValues(state.form.Vote),
tutorialVisible: !state.tutorial.location.isVisited,
normalSelected : ownProps.location.query.type == 'normal' || ownProps.location.query.type == undefined,
advancedSelected : ownProps.location.query.type == 'advanced',
- issueAssetSelected : ownProps.location.query.type == 'issueAsset',
+ voteSelected : ownProps.location.query.type == 'vote',
}
}
--- /dev/null
+import {
+ BaseNew,
+ TextField,
+ Autocomplete,
+ ObjectSelectorField,
+ AmountUnitField,
+ GasField
+} from 'features/shared/components'
+import {chainClient} from 'utility/environment'
+import {reduxForm} from 'redux-form'
+import React from 'react'
+import styles from './New.scss'
+import TxContainer from './NewTransactionsContainer/TxContainer'
+import actions from 'actions'
+import VoteConfirmModal from './VoteConfirmModal/VoteConfirmModal'
+import { voteTxActionBuilder} from '../../transactions'
+import {withNamespaces} from 'react-i18next'
+
+class Vote extends React.Component {
+ constructor(props) {
+ super(props)
+ this.connection = chainClient().connection
+ this.state = {
+ estimateGas:null,
+ address:null
+ }
+
+ this.submitWithValidation = this.submitWithValidation.bind(this)
+ this.disableSubmit = this.disableSubmit.bind(this)
+ }
+
+ disableSubmit() {
+ return !(this.state.estimateGas)
+ }
+
+ submitWithValidation(data) {
+ return new Promise((resolve, reject) => {
+ this.props.submitForm(Object.assign({}, data, {state: this.state, form: 'voteTx'}))
+ .then(() => {
+ this.props.closeModal()
+ this.props.destroyForm()
+ })
+ .catch((err) => {
+ if(err.message !== 'PasswordWrong'){
+ this.props.closeModal()
+ }
+ reject({_error: err})
+ })
+ })
+ }
+
+ confirmedTransaction(e){
+ e.preventDefault()
+ this.props.showModal(
+ <VoteConfirmModal
+ cancel={this.props.closeModal}
+ onSubmit={this.submitWithValidation}
+ gas={this.state.estimateGas}
+ btmAmountUnit={this.props.btmAmountUnit}
+ />
+ )
+ }
+
+ estimateNormalTransactionGas() {
+ const transaction = this.props.fields
+ const accountAlias = transaction.accountAlias.value
+ const accountId = transaction.accountId.value
+ const nodePubkey = transaction.nodePubkey.value
+ const amount = Number(transaction.amount.value)
+
+ const {t, i18n} = this.props
+
+ const noAccount = !accountAlias && !accountId
+
+ if ( nodePubkey === '' || amount === 0|| noAccount ) {
+ this.setState({estimateGas: null})
+ return
+ }
+
+
+ this.props.getAddress({accountAlias, accountId}).then(address => {
+ this.setState({address})
+ const actions = voteTxActionBuilder(transaction, Math.pow(10, 7), address)
+
+ return this.props.buildTransaction({actions, ttl: 1}).then(tmp => {
+ return this.props.estimateGasFee(tmp.data).then(resp => {
+ this.setState({estimateGas: Math.ceil(resp.data.totalNeu/100000)*100000})
+ }).catch(err =>{
+ throw err
+ })
+ }).catch(err =>{
+ throw err
+ })
+ }).catch(err =>{
+ this.setState({estimateGas: null, address: null})
+ const errorMsg = err.code && i18n.exists(`btmError.${err.code}`) && t(`btmError.${err.code}`) || err.msg
+ this.props.showError(new Error(errorMsg))
+ })
+
+ }
+
+ render() {
+ const {
+ fields: {accountId, accountAlias, nodePubkey, amount, gasLevel},
+ error,
+ submitting
+ } = this.props
+ const t = this.props.t;
+ [accountAlias, accountId,nodePubkey, amount].forEach(key => {
+ key.onBlur = this.estimateNormalTransactionGas.bind(this)
+ });
+
+ let submitLabel = t('transaction.vote.submit')
+
+ return (
+ <TxContainer
+ error={error}
+ onSubmit={(e)=>this.confirmedTransaction(e)}
+ submitting={submitting}
+ submitLabel= {submitLabel}
+ disabled={this.disableSubmit()}
+ className={styles.container}
+ >
+ <div className={styles.borderBottom}>
+ <label className={styles.title}>{t('transaction.vote.info')}</label>
+ <div className={`${styles.mainBox} `}>
+ <ObjectSelectorField
+ key='account-selector-field'
+ keyIndex='votetx-account'
+ title={t('form.account')}
+ aliasField={Autocomplete.AccountAlias}
+ fieldProps={{
+ id: accountId,
+ alias: accountAlias
+ }}
+ />
+ <div>
+ <TextField
+ key='asset-selector-field'
+ keyIndex='votetx-nodePubkey'
+ title={ t('form.vote')}
+ fieldProps={nodePubkey}
+ />
+ </div>
+ <div>
+ <AmountUnitField
+ key='asset-selector-field'
+ keyIndex='votetx-amount'
+ title={ t('transaction.vote.voteAmount')}
+ fieldProps={amount}
+ />
+ </div>
+ </div>
+
+
+ <label className={styles.title}>{t('transaction.normal.selectFee')}</label>
+ <div className={styles.txFeeBox}>
+ <GasField
+ gas={this.state.estimateGas}
+ fieldProps={gasLevel}
+ btmAmountUnit={this.props.btmAmountUnit}
+ />
+ <span className={styles.feeDescription}> {t('transaction.normal.feeDescription')}</span>
+ </div>
+ </div>
+ </TxContainer>
+ )
+ }
+}
+
+const validate = (values, props) => {
+ const errors = {gas: {}}
+ const t = props.t
+
+ // Numerical
+ if (values.amount && !/^\d+(\.\d+)?$/i.test(values.amount)) {
+ errors.amount = ( t('errorMessage.amountError') )
+ }
+ return errors
+}
+
+const mapDispatchToProps = (dispatch) => ({
+ showError: err => dispatch({type: 'ERROR', payload: err}),
+ closeModal: () => dispatch(actions.app.hideModal),
+ showModal: (body) => dispatch(actions.app.showModal(
+ body,
+ actions.app.hideModal,
+ null,
+ {
+ dialog: true,
+ noCloseBtn: true
+ }
+ )),
+ getAddress: actions.transaction.getAddresses,
+ estimateGasFee: actions.transaction.estimateGas,
+ buildTransaction: actions.transaction.buildTransaction,
+ ...BaseNew.mapDispatchToProps('transaction')(dispatch)
+})
+
+export default withNamespaces('translations') (BaseNew.connect(
+ BaseNew.mapStateToProps('transaction'),
+ mapDispatchToProps,
+ reduxForm({
+ form: 'Vote',
+ fields: [
+ 'accountAlias',
+ 'accountId',
+ 'nodePubkey',
+ 'amount',
+ 'gasLevel',
+ ],
+ validate,
+ initialValues: {
+ gasLevel: '1'
+ },
+ touchOnChange: true
+ })(Vote)
+))
--- /dev/null
+import React, { Component } from 'react'
+import {reduxForm} from 'redux-form'
+import {
+ PasswordField,
+ ErrorBanner,
+ SubmitIndicator
+} from 'features/shared/components'
+import { btmID } from 'utility/environment'
+import { normalizeBTMAmountUnit } from 'utility/buildInOutDisplay'
+import styles from './VoteConfirmModal.scss'
+import {withNamespaces} from 'react-i18next'
+
+
+class VoteConfirmModal extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const {
+ fields: { accountId, accountAlias, nodePubkey, amount, password, gasLevel },
+ handleSubmit,
+ submitting,
+ cancel,
+ error,
+ gas,
+ t,
+ btmAmountUnit,
+ } = this.props
+
+ const fee = Number(gasLevel.value * gas)
+
+ const Total = Number(amount.value) + fee
+ let submitLabel = t('transaction.vote.confirm')
+
+ const normalize = (value) => {
+ return normalizeBTMAmountUnit(btmID, value, btmAmountUnit)
+ }
+
+ return (
+ <div>
+ <h3>{ t('transaction.vote.confirm') }</h3>
+ <table className={styles.table}>
+ <tbody>
+ <tr>
+ <td className={styles.colLabel}>{ t('form.account') }</td>
+ <td> <span>{accountAlias.value || accountId.value}</span></td>
+ </tr>
+ <tr>
+ <td className={styles.colLabel}>{ t('transaction.vote.for') }</td>
+ <td> <span>{nodePubkey.value}</span> </td>
+ </tr>
+ <tr>
+ <td className={styles.colLabel}>{ t('transaction.vote.voteAmount') }</td>
+ <td> <code>{normalize(amount.value)}</code> </td>
+ </tr>
+
+ <tr>
+ <td className={styles.colLabel}>{t('form.fee')}</td>
+ <td> <code>{normalize(fee)}</code> </td>
+ </tr>
+
+ <tr>
+ <td className={styles.colLabel}>{t('transaction.normal.total')}</td>
+ <td> <code>{normalize(Total)}</code> </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <hr className={styles.hr}/>
+
+ <form onSubmit={handleSubmit}>
+ <div>
+ <label>{t('key.password')}</label>
+ <PasswordField
+ placeholder={t('key.passwordPlaceholder')}
+ fieldProps={password}
+ />
+ </div>
+
+ {error && error.message === 'PasswordWrong' &&
+ <ErrorBanner
+ title={t('form.errorTitle')}
+ error={t('errorMessage.password')} />}
+
+ <div className={styles.btnGroup}>
+ <div>
+ <button type='submit' className='btn btn-primary'
+ disabled={submitting}>
+ {submitLabel}
+ </button>
+
+ {submitting &&
+ <SubmitIndicator className={styles.submitIndicator} />
+ }
+ </div>
+ <button type='button' className='btn btn-default' onClick={cancel}>
+ <i/> {t('form.cancel')}
+ </button>
+ </div>
+ </form>
+ </div>
+ )
+
+ }
+}
+
+const validate = values => {
+ const errors = {}
+ if (!values.password) {
+ errors.password = 'Required'
+ }
+ return errors
+}
+
+export default withNamespaces('translations') (reduxForm({
+ form: 'Vote',
+ fields:[
+ 'accountAlias',
+ 'accountId',
+ 'nodePubkey',
+ 'amount',
+ 'gasLevel',
+ 'password'
+ ],
+ destroyOnUnmount: false,
+ validate
+})(VoteConfirmModal))
\ No newline at end of file
--- /dev/null
+.submitIndicator{
+ float:right;
+}
+
+.btnGroup{
+ display: flex;
+ >div{
+ width: 50%;
+ button{
+ width: 100%;
+ }
+ }
+
+ >button{
+ width: 50%;
+ margin-left: $gutter-size;
+ }
+}
+
+.hr{
+ margin-top: 11px;
+ margin-bottom: 11px;
+}
+
+.table{
+ word-break: break-all;
+ width: 100%;
+ td{
+ padding: 4px 0px;
+ }
+}
+
+.colLabel{
+ width: 20%;
+ color: $text-strong-color;
+ font-weight: 500;
+}
+
+.unit{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ width: 130px;
+ display: inline-block;
+}
return actions
}
+
+export const voteTxActionBuilder = (transaction, gas, address) =>{
+ const accountAlias = transaction.accountAlias.value || transaction.accountAlias
+ const accountId = transaction.accountId.value || transaction.accountId
+ const nodePubkey = transaction.nodePubkey.value || transaction.nodePubkey
+ const amount = transaction.amount.value || transaction.amount
+
+ const spendAction = {
+ accountAlias,
+ accountId,
+ assetId: btmID,
+ amount,
+ type: 'spend_account'
+ }
+
+
+ const gasAction = {
+ accountAlias,
+ accountId,
+ asset_id:btmID,
+ amount: gas,
+ type: 'spend_account'
+ }
+
+ const voteAction ={
+ amount,
+ asset_id:btmID,
+ address,
+ vote: nodePubkey,
+ type:'vote_output'
+ }
+
+ const actions = [spendAction, gasAction, voteAction]
+
+ return actions
+}
"decimals":"Decimals",
"reissueTitle":"Token reissuable",
"reissueTrue":"Able to be reissued",
- "reissueFalse":"Unable to be reissued"
+ "reissueFalse":"Unable to be reissued",
+ "vote": "Node Public Key"
},
"xpub":{
"methodOptions" : {
"unsaveWarning":"Your work is not saved! Are you sure you want to leave?",
"new":"New transaction",
"normal":"Normal",
+ "vote": "Vote",
"submit":"Submit transaction",
"advanced" :"Advanced"
},
"accountAddress":"Gas Paid Account Address",
"gasAccount":"Gas Paid Account",
"signTx":"Sign Transaction"
+ },
+ "vote":{
+ "info":"Vote Information",
+ "voteAmount":"Amount Vote",
+ "for":"Vote for",
+ "submit":"Submit Vote",
+ "confirm":"Confirm Vote"
}
},
"balances":{
"decimals":"精度",
"reissueTitle":"是否可重复发行",
"reissueTrue":"可重复发行",
- "reissueFalse":"不可重复发行"
+ "reissueFalse":"不可重复发行",
+ "vote": "节点公钥"
},
"xpub":{
"methodOptions" : {
"new":"新建交易",
"normal":"简单交易",
"advanced" :"高级交易",
+ "vote": "投票",
"submit":"提交交易"
},
"normal":{
"accountAddress":"Gas付款账户地址",
"gasAccount":"Gas付款账户",
"signTx":"签名提交交易"
+ },
+ "vote":{
+ "info":"投票信息",
+ "voteAmount":"投票数量",
+ "for":"投票给",
+ "submit":"提交投票",
+ "confirm":"确认投票"
}
},
"balances":{