diff --git a/packages/app-staking/src/Account/Bond.tsx b/packages/app-staking/src/Account/Bond.tsx index 2203c8fd10..50bba24fdd 100644 --- a/packages/app-staking/src/Account/Bond.tsx +++ b/packages/app-staking/src/Account/Bond.tsx @@ -3,15 +3,21 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-api/types'; +import { CalculateBalanceProps } from '../types'; import BN from 'bn.js'; import React from 'react'; +import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import { Button, InputAddress, InputBalance, Modal, TxButton, Dropdown } from '@polkadot/ui-app'; +import { withCalls, withApi, withMulti } from '@polkadot/ui-api'; +import { calcSignatureLength } from '@polkadot/ui-signer/Checks'; +import { ZERO_BALANCE, ZERO_FEES } from '@polkadot/ui-signer/Checks/constants'; import translate from '../translate'; import ValidateController from './ValidateController'; -type Props = I18nProps & { +type Props = I18nProps & ApiProps & CalculateBalanceProps & { accountId: string, controllerId?: string | null, isOpen: boolean, @@ -22,7 +28,9 @@ type State = { bondValue?: BN, controllerError: string | null, controllerId: string, - destination: number + destination: number, + extrinsic: SubmittableExtrinsic | null, + maxBalance?: BN }; const stashOptions = [ @@ -31,6 +39,8 @@ const stashOptions = [ { text: 'Controller account', value: 2 } ]; +const ZERO = new BN(0); + class Bond extends React.PureComponent { state: State; @@ -42,15 +52,31 @@ class Bond extends React.PureComponent { this.state = { controllerError: null, controllerId: controllerId ? controllerId.toString() : accountId, - destination: 0 + destination: 0, + extrinsic: null }; } + componentDidUpdate (prevProps: Props, prevState: State) { + const { balances_fees } = this.props; + const { controllerId, destination, extrinsic } = this.state; + + const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0); + + if ((controllerId && prevState.controllerId !== controllerId) || + (prevState.destination !== destination) || + (balances_fees !== prevProps.balances_fees) || + hasLengthChanged + ) { + this.setMaxBalance(); + } + } + render () { const { accountId, isOpen, onClose, t } = this.props; - const { bondValue, controllerError, controllerId, destination } = this.state; - const hasValue = !!bondValue && bondValue.gtn(0); - const canSubmit = hasValue && !controllerError; + const { bondValue, controllerError, controllerId, extrinsic, maxBalance } = this.state; + const hasValue = !!bondValue && bondValue.gtn(0) && (!maxBalance || bondValue.lte(maxBalance)); + const canSubmit = hasValue && !controllerError && !!controllerId; if (!isOpen) { return null; @@ -78,8 +104,7 @@ class Bond extends React.PureComponent { isPrimary label={t('Bond')} onClick={onClose} - params={[controllerId, bondValue, destination]} - tx='staking.bond' + extrinsic={extrinsic} /> @@ -89,7 +114,7 @@ class Bond extends React.PureComponent { private renderContent () { const { accountId, t } = this.props; - const { controllerId, controllerError, bondValue, destination } = this.state; + const { controllerId, controllerError, bondValue, destination, maxBalance } = this.state; const hasValue = !!bondValue && bondValue.gtn(0); return ( @@ -124,7 +149,9 @@ class Bond extends React.PureComponent { help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')} isError={!hasValue} label={t('value bonded')} + maxValue={maxBalance} onChange={this.onChangeValue} + withMax /> { ); } + private nextState (newState: Partial): void { + this.setState((prevState: State): State => { + const { api } = this.props; + const { bondValue = prevState.bondValue, controllerError = prevState.controllerError, controllerId = prevState.controllerId, destination = prevState.destination, maxBalance = prevState.maxBalance } = newState; + const extrinsic = (bondValue && controllerId) + ? api.tx.staking.bond(controllerId, bondValue, destination) + : null; + + return { + bondValue, + controllerError, + controllerId, + destination, + extrinsic, + maxBalance + }; + }); + } + + private setMaxBalance = () => { + const { api, system_accountNonce = ZERO, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE } = this.props; + const { controllerId, destination } = this.state; + + const { transactionBaseFee, transactionByteFee } = balances_fees; + const { freeBalance } = balances_all; + + let prevMax = new BN(0); + let maxBalance = new BN(1); + let extrinsic; + + console.log(Object.entries(balances_all) + .reduce((obj, [key, bn]) => { + return { + ...obj, + [key]: bn.toString() + }; + }, {})); + + while (!prevMax.eq(maxBalance)) { + prevMax = maxBalance; + + extrinsic = controllerId && destination + ? api.tx.staking.bond(controllerId, prevMax, destination) + : null; + + const txLength = calcSignatureLength(extrinsic, system_accountNonce); + + const fees = transactionBaseFee + .add(transactionByteFee.muln(txLength)); + + console.log(fees.toString()); + + maxBalance = new BN(freeBalance).sub(fees); + } + + this.nextState({ + extrinsic, + maxBalance + }); + } + private onChangeController = (controllerId: string) => { - this.setState({ controllerId }); + this.nextState({ controllerId }); } private onChangeDestination = (destination: number) => { - this.setState({ destination }); + this.nextState({ destination }); } private onChangeValue = (bondValue?: BN) => { - this.setState({ bondValue }); + this.nextState({ bondValue }); } private onControllerError = (controllerError: string | null) => { @@ -157,4 +245,13 @@ class Bond extends React.PureComponent { } } -export default translate(Bond); +export default withMulti( + Bond, + translate, + withApi, + withCalls( + 'derive.balances.fees', + ['derive.balances.all', { paramName: 'accountId' }], + ['query.system.accountNonce', { paramName: 'accountId' }] + ) +); diff --git a/packages/app-staking/src/Account/BondExtra.tsx b/packages/app-staking/src/Account/BondExtra.tsx index c17162f8bf..855c16617d 100644 --- a/packages/app-staking/src/Account/BondExtra.tsx +++ b/packages/app-staking/src/Account/BondExtra.tsx @@ -3,30 +3,58 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-api/types'; +import { CalculateBalanceProps } from '../types'; import BN from 'bn.js'; import React from 'react'; import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/ui-app'; +import { Option, StakingLedger } from '@polkadot/types'; +import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; +import { withCalls, withApi, withMulti } from '@polkadot/ui-api'; +import { calcSignatureLength } from '@polkadot/ui-signer/Checks'; +import { ZERO_BALANCE, ZERO_FEES } from '@polkadot/ui-signer/Checks/constants'; import translate from '../translate'; -type Props = I18nProps & { +type Props = I18nProps & ApiProps & CalculateBalanceProps & { accountId: string, + controllerId: string, isOpen: boolean, - onClose: () => void + onClose: () => void, + staking_ledger?: Option }; type State = { - maxAdditional?: BN + maxAdditional?: BN, + extrinsic: SubmittableExtrinsic | null, + maxBalance?: BN }; +const ZERO = new BN(0); + class BondExtra extends React.PureComponent { - state: State = {}; + state: State = { + extrinsic: null + }; + + componentDidUpdate (prevProps: Props, prevState: State) { + const { balances_fees } = this.props; + const { extrinsic } = this.state; + + const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0); + + if ((balances_fees !== prevProps.balances_fees) || + hasLengthChanged + ) { + this.setMaxBalance(); + } + } render () { - const { accountId, isOpen, onClose, t } = this.props; - const { maxAdditional } = this.state; - const canSubmit = !!maxAdditional && maxAdditional.gtn(0); + const { accountId, balances_all = ZERO_BALANCE, isOpen, onClose, t } = this.props; + const { extrinsic, maxAdditional, maxBalance = balances_all.availableBalance } = this.state; + const canSubmit = !!maxAdditional && maxAdditional.gtn(0) && maxAdditional.lte(maxBalance); if (!isOpen) { return null; @@ -54,8 +82,7 @@ class BondExtra extends React.PureComponent { isPrimary label={t('Bond')} onClick={onClose} - params={[maxAdditional]} - tx='staking.bondExtra' + extrinsic={extrinsic} /> @@ -65,6 +92,7 @@ class BondExtra extends React.PureComponent { private renderContent () { const { accountId, t } = this.props; + const { maxBalance } = this.state; return ( <> @@ -83,16 +111,81 @@ class BondExtra extends React.PureComponent { className='medium' help={t('The maximum amount to increase the bonded value, this is adjusted using the available free funds on the account.')} label={t('max additional value')} + maxValue={maxBalance} onChange={this.onChangeValue} + withMax /> ); } + private nextState (newState: Partial): void { + this.setState((prevState: State): State => { + const { api } = this.props; + const { maxAdditional = prevState.maxAdditional, maxBalance = prevState.maxBalance } = newState; + const extrinsic = (maxAdditional && maxAdditional.gte(ZERO)) + ? api.tx.staking.bondExtra(maxAdditional) + : null; + + return { + maxAdditional, + extrinsic, + maxBalance + }; + }); + } + + private setMaxBalance = () => { + const { api, system_accountNonce = ZERO, balances_fees = ZERO_FEES, balances_all = ZERO_BALANCE, staking_ledger } = this.props; + const { maxAdditional } = this.state; + + const { transactionBaseFee, transactionByteFee } = balances_fees; + const { freeBalance } = balances_all; + + let prevMax = new BN(0); + let maxBalance = new BN(1); + let extrinsic; + + let bonded = new BN(0); + if (staking_ledger && !staking_ledger.isNone) { + bonded = staking_ledger.unwrap().active; + } + + while (!prevMax.eq(maxBalance)) { + prevMax = maxBalance; + + extrinsic = (maxAdditional && maxAdditional.gte(ZERO)) + ? api.tx.staking.bondExtra(maxAdditional.sub(bonded)) + : null; + + const txLength = calcSignatureLength(extrinsic, system_accountNonce); + + const fees = transactionBaseFee + .add(transactionByteFee.muln(txLength)); + + maxBalance = new BN(freeBalance).sub(fees).sub(bonded); + } + + this.nextState({ + extrinsic, + maxBalance + }); + } + private onChangeValue = (maxAdditional?: BN) => { - this.setState({ maxAdditional }); + this.nextState({ maxAdditional }); } } -export default translate(BondExtra); +export default withMulti( + BondExtra, + translate, + withApi, + withCalls( + 'derive.balances.fees', + ['derive.balances.all', { paramName: 'accountId' }], + ['query.system.accountNonce', { paramName: 'accountId' }], + ['query.staking.ledger', { paramName: 'controllerId' }] + ) +); diff --git a/packages/app-staking/src/Account/Unbond.tsx b/packages/app-staking/src/Account/Unbond.tsx index 8ebf579970..b57215c4a3 100644 --- a/packages/app-staking/src/Account/Unbond.tsx +++ b/packages/app-staking/src/Account/Unbond.tsx @@ -2,26 +2,40 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import BN from 'bn.js'; -import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/ui-app'; import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-api/types'; + +import BN from 'bn.js'; import React from 'react'; +import { AccountId, Option, StakingLedger } from '@polkadot/types'; +import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/ui-app'; +import { withCalls, withApi, withMulti } from '@polkadot/ui-api'; import translate from '../translate'; -type Props = I18nProps & { - controllerId?: string | null, +type Props = I18nProps & ApiProps & { + controllerId?: AccountId | null, isOpen: boolean, - onClose: () => void + onClose: () => void, + staking_ledger?: Option }; type State = { + maxBalance?: BN maxUnbond?: BN }; class Unbond extends React.PureComponent { state: State = {}; + componentDidUpdate (prevProps: Props) { + const { staking_ledger } = this.props; + + if (staking_ledger !== prevProps.staking_ledger) { + this.setMaxBalance(); + } + } + render () { const { controllerId, isOpen, onClose, t } = this.props; const { maxUnbond } = this.state; @@ -64,6 +78,7 @@ class Unbond extends React.PureComponent { private renderContent () { const { controllerId, t } = this.props; + const { maxBalance } = this.state; return ( <> @@ -75,23 +90,57 @@ class Unbond extends React.PureComponent { className='medium' defaultValue={controllerId} isDisabled - label={t('controler account')} + label={t('controller account')} /> ); } + private nextState (newState: Partial): void { + this.setState((prevState: State): State => { + const { maxUnbond = prevState.maxUnbond, maxBalance = prevState.maxBalance } = newState; + + return { + maxUnbond, + maxBalance + }; + }); + } + + private setMaxBalance = () => { + const { staking_ledger } = this.props; + + if (!staking_ledger || staking_ledger.isNone) { + return; + } + + const { active: maxBalance } = staking_ledger.unwrap(); + + this.nextState({ + maxBalance + }); + } + private onChangeValue = (maxUnbond?: BN) => { - this.setState({ maxUnbond }); + this.nextState({ maxUnbond }); } } -export default translate(Unbond); +export default withMulti( + Unbond, + translate, + withApi, + withCalls( + ['query.staking.ledger', { paramName: 'controllerId' }] + ) +); diff --git a/packages/app-staking/src/Account/index.tsx b/packages/app-staking/src/Account/index.tsx index 6f7625be13..b6bce12ba0 100644 --- a/packages/app-staking/src/Account/index.tsx +++ b/packages/app-staking/src/Account/index.tsx @@ -113,12 +113,13 @@ class Account extends React.PureComponent { } private renderBondExtra () { - const { accountId } = this.props; + const { accountId, controllerId } = this.props; const { isBondExtraOpen } = this.state; return ( diff --git a/packages/app-staking/src/types.ts b/packages/app-staking/src/types.ts index a15b107e33..8a37cd4384 100644 --- a/packages/app-staking/src/types.ts +++ b/packages/app-staking/src/types.ts @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import BN from 'bn.js'; -import { DerivedBalancesMap } from '@polkadot/api-derive/types'; +import { DerivedFees, DerivedBalances, DerivedBalancesMap } from '@polkadot/api-derive/types'; import { AccountId, BlockNumber } from '@polkadot/types'; export type Nominators = { @@ -19,6 +19,12 @@ export type ComponentProps = { validators: Array }; +export type CalculateBalanceProps = { + balances_fees?: DerivedFees, + balances_all?: DerivedBalances, + system_accountNonce?: BN +}; + export type RecentlyOffline = Array<[AccountId, BlockNumber, BN]>; export type RecentlyOfflineMap = { diff --git a/packages/app-transfer/src/Transfer.tsx b/packages/app-transfer/src/Transfer.tsx index 6a1128a3b0..1493be5ff0 100644 --- a/packages/app-transfer/src/Transfer.tsx +++ b/packages/app-transfer/src/Transfer.tsx @@ -4,26 +4,33 @@ import { I18nProps } from '@polkadot/ui-app/types'; import { ApiProps } from '@polkadot/ui-api/types'; +import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types'; import BN from 'bn.js'; import React from 'react'; import styled from 'styled-components'; -import { IExtrinsic } from '@polkadot/types/types'; +import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import { AddressSummary, InputAddress, InputBalance } from '@polkadot/ui-app'; -import { withApi, withMulti } from '@polkadot/ui-api'; +import { withApi, withCalls, withMulti } from '@polkadot/ui-api'; import keyring from '@polkadot/ui-keyring'; -import Checks from '@polkadot/ui-signer/Checks'; +import Checks, { calcSignatureLength } from '@polkadot/ui-signer/Checks'; +import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants'; import Submit from './Submit'; import translate from './translate'; -type Props = I18nProps & ApiProps & {}; +type Props = I18nProps & ApiProps & { + balances_fees?: DerivedFees, + balances_votingBalance?: DerivedBalances, + system_accountNonce?: BN +}; type State = { accountId: string | null, amount: BN, - extrinsic: IExtrinsic | null, + extrinsic: SubmittableExtrinsic | null, hasAvailable: boolean, + maxBalance?: BN, recipientId: string | null }; @@ -55,12 +62,28 @@ class Transfer extends React.PureComponent { amount: ZERO, extrinsic: null, hasAvailable: true, + maxBalance: ZERO, recipientId: null }; + componentDidUpdate (prevProps: Props, prevState: State) { + const { balances_fees } = this.props; + const { accountId, extrinsic, recipientId } = this.state; + + const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0); + + if ((accountId && prevState.accountId !== accountId) || + (recipientId && prevState.recipientId !== recipientId) || + (balances_fees !== prevProps.balances_fees) || + hasLengthChanged + ) { + this.setMaxBalance(); + } + } + render () { const { t } = this.props; - const { accountId, extrinsic, recipientId, hasAvailable } = this.state; + const { accountId, extrinsic, recipientId, hasAvailable, maxBalance } = this.state; return ( @@ -84,7 +107,9 @@ class Transfer extends React.PureComponent { help={t('Type the amount you want to transfer. Note that you can select the unit on the right e.g sending 1 mili is equivalent to sending 0.001.')} isError={!hasAvailable} label={t('amount')} + maxValue={maxBalance} onChange={this.onChangeAmount} + withMax /> { private nextState (newState: Partial): void { this.setState((prevState: State): State => { const { api } = this.props; - const { accountId = prevState.accountId, amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable } = newState; + const { accountId = prevState.accountId, amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance } = newState; const extrinsic = accountId && recipientId ? api.tx.balances.transfer(recipientId, amount) : null; @@ -138,11 +163,58 @@ class Transfer extends React.PureComponent { amount, extrinsic, hasAvailable, + maxBalance, recipientId }; }); } + private setMaxBalance = () => { + const { api, balances_fees = ZERO_FEES } = this.props; + const { accountId, recipientId } = this.state; + + if (!accountId || !recipientId) { + return; + } + + void api.query.system.accountNonce(accountId, (accountNonce) => { + + const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees; + + void api.derive.balances.all(accountId, ({ availableBalance: senderBalance }) => { + void api.derive.balances.all(recipientId, ({ availableBalance: recipientBalance }) => { + + let prevMax = new BN(0); + let maxBalance = new BN(1); + let extrinsic; + + while (!prevMax.eq(maxBalance)) { + prevMax = maxBalance; + + extrinsic = accountId && recipientId + ? api.tx.balances.transfer(recipientId, prevMax) + : null; + + const txLength = calcSignatureLength(extrinsic, accountNonce); + + const fees = transactionBaseFee + .add(transactionByteFee.muln(txLength)) + .add(transferFee) + .add(senderBalance.isZero() ? creationFee : ZERO) + .add(recipientBalance.isZero() ? creationFee : ZERO); + + maxBalance = new BN(senderBalance).sub(fees); + } + + this.nextState({ + extrinsic, + maxBalance + }); + }); + }); + }); + } + private onChangeFrom = (accountId: string) => { this.nextState({ accountId }); } @@ -163,5 +235,8 @@ class Transfer extends React.PureComponent { export default withMulti( Transfer, translate, - withApi + withApi, + withCalls( + 'derive.balances.fees' + ) ); diff --git a/packages/ui-app/src/Dropdown.tsx b/packages/ui-app/src/Dropdown.tsx index 0e55093bf6..3c06be448f 100644 --- a/packages/ui-app/src/Dropdown.tsx +++ b/packages/ui-app/src/Dropdown.tsx @@ -14,6 +14,7 @@ import Labelled from './Labelled'; type Props