import { DeriveFees } from '@polkadot/api-derive/types' ; import { SubmittableExtrinsic } from '@polkadot/api/types' ; import { createType } from '@polkadot/types' ; import { AccountInfo } from '@polkadot/types/interfaces' ; import { AccountsContext , ApiRxContext , ExtrinsicDetails , handler , TxQueueContext , } from '@substrate/context' ; import { Spinner } from '@substrate/design-system' ; import { BalanceDisplay , Dropdown , DropdownProps , ErrorText , Input , InputAddress , List , Margin , Modal , Stacked , } from '@substrate/ui-components' ; import BN from 'bn.js' ; import { navigate } from 'gatsby' ; import React , { useCallback , useContext , useEffect , useState } from 'react' ; import { take } from 'rxjs/operators' ; import { validateFees } from '../util/validateExtrinsic' ; import { Button , SubHeader } from './index' ; enum RewardDestination { 'Stash' , 'Staked' , 'Controller' } const rewardDestinationOptions = [ { key : 'Stake' , text : 'Stash (increase amount at stake)' , value : 'Staked' , } , { key : 'Stash' , text : 'Stash (do not increase amount at stake)' , value : 'Stash' , } , { key : 'Controller' , text : 'Controller' , value : 'Controller' , } , ] ; type Error = string ; const BondingModal = ( ) : React . ReactElement => { const { state : { accountBalanceMap , allAccounts , currentAccount , loadingBalances } , } = useContext ( AccountsContext ) ; const { api , isApiReady } = useContext ( ApiRxContext ) ; const { enqueue , signAndSubmit , state : { txQueue } , } = useContext ( TxQueueContext ) ; const [ accountForController , setAccountForController ] = useState ( currentAccount ) ; const [ accountForStash , setAccountForStash ] = useState ( currentAccount ) ; const [ accountNonce , setAccountNonce ] = useState < AccountInfo > ( ) ; const [ allFees , setAllFees ] = useState < BN > ( ) ; const [ allTotal , setAllTotal ] = useState < BN > ( ) ; const [ bondAmount , setBondAmount ] = useState < string > ( '' ) ; const [ bondingError , setBondingError ] = useState < Error > ( ) ; const [ extrinsic , setExtrinsic ] = useState < SubmittableExtrinsic < 'rxjs' > > ( ) ; const [ fees , setFees ] = useState < DeriveFees > ( ) ; const [ rewardDestination , setRewardDestination ] = useState < RewardDestination > ( RewardDestination . Staked ) ; const [ isSubmitted , setIsSubmitted ] = useState ( false ) ; const [ txId , setTxId ] = useState < number > ( ) ; useEffect ( ( ) => { if ( api && isApiReady ) { const sub = api . derive . balances . fees ( ) . pipe ( take ( 1 ) ) . subscribe ( ( result ) => setFees ( result ) ) ; return ( ) : void => sub . unsubscribe ( ) ; } } , [ api , isApiReady ] ) ; useEffect ( ( ) => { if ( api && isApiReady && accountForStash ) { const sub = api . query . system . account < AccountInfo > ( accountForStash ) . pipe ( take ( 1 ) ) . subscribe ( ( result ) => setAccountNonce ( result ) ) ; return ( ) : void => sub . unsubscribe ( ) ; } } , [ accountForStash , api , isApiReady ] ) ; const checkFees = useCallback ( ( ) : void => { if ( isApiReady && accountForStash && accountForController && accountNonce && bondAmount && extrinsic && fees ) { const [ feeErrors , total , fee ] = validateFees ( accountNonce , new BN ( bondAmount ) , accountBalanceMap [ accountForStash ] , extrinsic , fees ) ; setAllTotal ( total ) ; setAllFees ( fee ) ; if ( feeErrors ) { setBondingError ( feeErrors [ 0 ] ) ; } else { setBondingError ( undefined ) ; } } } , [ accountBalanceMap , accountForStash , accountForController , accountNonce , bondAmount , extrinsic , fees , isApiReady , ] ) ; const checkUserInputs = useCallback ( ( ) : void => { if ( ! accountForStash ) { setBondingError ( 'Please select an account to use as your stash.' ) ; return ; } if ( ! accountForController ) { setBondingError ( 'Please select an account to use as your controller.' ) ; return ; } if ( accountForStash === accountForController ) { setBondingError ( 'Please use different accounts for stash and controller' ) ; return ; } if ( new BN ( bondAmount ) . lten ( 1000 ) ) { setBondingError ( 'Minimum bond amount is 1000 Units' ) ; return ; } if ( ! rewardDestination ) { setBondingError ( 'Please select a reward destination.' ) ; return ; } setBondingError ( undefined ) ; } , [ accountForStash , accountForController , bondAmount , rewardDestination ] ) ; useEffect ( ( ) => { if ( accountForController && api && isApiReady ) { const submitBondExtrinsic = api . tx . staking . bond ( accountForController , new BN ( bondAmount ) , rewardDestination ) ; setExtrinsic ( submitBondExtrinsic ) ; } } , [ accountForController , api , bondAmount , isApiReady , rewardDestination ] ) ; useEffect ( ( ) => { if ( currentAccount ) { setAccountForStash ( currentAccount ) ; setAccountForController ( currentAccount ) ; } } , [ currentAccount ] ) ; useEffect ( ( ) => { if ( api ) { checkUserInputs ( ) ; checkFees ( ) ; } } , [ api , accountForStash , accountForController , bondAmount , checkFees , checkUserInputs , rewardDestination , ] ) ; useEffect ( ( ) => { if ( txId !== undefined && ! isSubmitted ) { signAndSubmit ( txId ) ; setIsSubmitted ( true ) ; navigate ( '/accounts' ) ; } } , [ isSubmitted , signAndSubmit , txId , txQueue ] ) ; const selectStash = useCallback ( ( address ) : void => { setAccountForStash ( address ) ; } , [ ] ) ; const selectController = useCallback ( ( address ) : void => { setAccountForController ( address ) ; } , [ ] ) ; const handleSetRewardDestination = useCallback ( ( _event , { value } ) => { setRewardDestination ( value as RewardDestination ) ; } , [ ] ) ; const signAndSubmitBond = useCallback ( ( ) => { if ( api && accountForController && accountForStash && allFees && allTotal && extrinsic ) { const details = { allFees , allTotal , amount : createType ( api . registry , 'Balance' , bondAmount ) , methodCall : 'staking.bond' , senderPair : accountForStash , } ; const id = enqueue ( extrinsic , details ) ; setTxId ( id ) ; } } , [ api , accountForController , accountForStash , allFees , allTotal , bondAmount , extrinsic , enqueue , ] ) ; return ( < Modal closeIcon closeOnDimmerClick dimmer trigger = { < List . Item > < Button neutral > New Bond < /Button> < /List.Item>}> < Modal . Header > Bonding Preferences < /Modal.Header> < Modal . Content > < Stacked alignItems = 'stretch' justifyContent = 'space-between' > < SubHeader > Choose Stash : < /SubHeader> { accountForStash ? ( < > < InputAddress accounts = { allAccounts } fromKeyring = { false } onChangeAddress = { selectStash } value = { accountForStash } width = '175px' / > { loadingBalances ? ( < Spinner active inline / > ) : ( < BalanceDisplay allBalances = { accountBalanceMap [ accountForStash ] } / > ) } < />) : (<Spinner active inline/ > ) } < SubHeader > Choose Controller : < /SubHeader> { accountForController ? ( < > < InputAddress accounts = { allAccounts } fromKeyring = { false } onChangeAddress = { selectController } value = { accountForController } width = '175px' / > { loadingBalances ? ( < Spinner active inline / > ) : ( < BalanceDisplay allBalances = { accountBalanceMap [ accountForController ] } / > ) } < />) : (<Spinner active inline/ > ) } < Margin top / > < SubHeader > Reward Destination < /SubHeader> < Dropdown fluid placeholder = 'Reward Destination' selection onChange = { handleSetRewardDestination } options = { rewardDestinationOptions } / > < Margin top / > < SubHeader > Bond Amount : < /SubHeader> < Input fluid label = 'UNIT' labelPosition = 'right' min = { 0 } onChange = { handler ( setBondAmount ) } placeholder = 'e.g. 1.00' type = 'number' value = { bondAmount } / > < Modal . Description > { isSubmitted ? ( < Spinner active inline / > ) : ( < Button float = 'right' size = 'big' onClick = { signAndSubmitBond } > Submit Bond < /Button>)} < ErrorText > { bondingError } < /ErrorText> < /Modal.Description> < /Stacked> < /Modal.Content> < /Modal>); } ; export { BondingModal } ;