diff --git a/src/actions/transactionActions.ts b/src/actions/transactionActions.ts index 9baf197c..21c79e44 100644 --- a/src/actions/transactionActions.ts +++ b/src/actions/transactionActions.ts @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges UI. If not, see . +import { BalanceState } from '../types/accountTypes'; import { CreateType } from '../types/apiCallsTypes'; import { SourceTargetState } from '../types/sourceTargetTypes'; @@ -32,7 +33,8 @@ enum TransactionActionTypes { SET_RECEIVER = 'SET_RECEIVER', SET_RECEIVER_ADDRESS = 'SET_RECEIVER_ADDRESS', SET_RECEIVER_VALIDATION = 'SET_RECEIVER_VALIDATION', - SET_SENDER_AND_ACTION = 'SET_SENDER_AND_ACTION', + SET_SENDER = 'SET_SENDER', + SET_ACTION = 'SET_ACTION', SET_PAYLOAD_ESTIMATED_FEE = 'SET_PAYLOAD_ESTIMATED_FEE', CREATE_TRANSACTION_STATUS = 'CREATE_TRANSACTION_STATUS', UPDATE_CURRENT_TRANSACTION_STATUS = 'UPDATE_CURRENT_TRANSACTION_STATUS', @@ -42,6 +44,8 @@ enum TransactionActionTypes { SET_WEIGHT_INPUT = 'SET_WEIGHT_INPUT', UPDATE_TRANSACTIONS_STATUS = 'UPDATE_TRANSACTIONS_STATUS', SET_BATCH_PAYLOAD_ESTIMATED_FEE = 'SET_BATCH_PAYLOAD_ESTIMATED_FEE', + UPDATE_SENDER_BALANCES = 'UPDATE_SENDER_BALANCES', + SET_TRANSFER_TYPE = 'SET_TRANSFER_TYPE', RESET = 'RESET' } @@ -58,7 +62,9 @@ const setPayloadEstimatedFee = ( payloadEstimatedFeeLoading: boolean, sourceTargetDetails: SourceTargetState, createType: CreateType, - isBridged: boolean + isBridged: boolean, + senderAccountBalance: BalanceState | null, + senderCompanionAccountBalance: BalanceState | null ) => ({ payload: { payloadEstimatedFee, @@ -66,7 +72,9 @@ const setPayloadEstimatedFee = ( payloadEstimatedFeeLoading, sourceTargetDetails, createType, - isBridged + isBridged, + senderAccountBalance, + senderCompanionAccountBalance }, type: TransactionActionTypes.SET_PAYLOAD_ESTIMATED_FEE }); @@ -114,13 +122,14 @@ const setTransactionRunning = (transactionRunning: boolean) => ({ type: TransactionActionTypes.SET_TRANSACTION_RUNNING }); -type SenderAndActionInput = { - senderAccount: string | null; - action: TransactionTypes; -}; -const setSenderAndAction = ({ senderAccount, action }: SenderAndActionInput) => ({ - payload: { senderAccount, action }, - type: TransactionActionTypes.SET_SENDER_AND_ACTION +const setAction = (action: TransactionTypes) => ({ + payload: { action }, + type: TransactionActionTypes.SET_ACTION +}); + +const setSender = (senderAccount: string | null) => ({ + payload: { senderAccount }, + type: TransactionActionTypes.SET_SENDER }); const setRemarkInput = (remarkInput: string | null) => ({ @@ -143,8 +152,24 @@ const setBatchedEvaluationPayloadEstimatedFee = (batchedTransactionState: Transa type: TransactionActionTypes.SET_BATCH_PAYLOAD_ESTIMATED_FEE }); +type UpdateSenderBalances = { + senderAccountBalance: BalanceState | null; + senderCompanionAccountBalance: BalanceState | null; +}; + +const updateSenderBalances = ({ senderAccountBalance, senderCompanionAccountBalance }: UpdateSenderBalances) => ({ + payload: { senderAccountBalance, senderCompanionAccountBalance }, + type: TransactionActionTypes.UPDATE_SENDER_BALANCES +}); + +const setTransferType = (transferType: TransactionTypes) => ({ + payload: { transferType }, + type: TransactionActionTypes.SET_TRANSFER_TYPE +}); + const TransactionActionCreators = { - setSenderAndAction, + setSender, + setAction, setReceiverAddress, setReceiver, setTransferAmount, @@ -157,6 +182,8 @@ const TransactionActionCreators = { updateTransactionStatus, updateTransactionsStatus, setBatchedEvaluationPayloadEstimatedFee, + updateSenderBalances, + setTransferType, reset }; diff --git a/src/components/EstimatedFee.tsx b/src/components/EstimatedFee.tsx index 83556a87..98193e06 100644 --- a/src/components/EstimatedFee.tsx +++ b/src/components/EstimatedFee.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { useSourceTarget } from '../contexts/SourceTargetContextProvider'; import { useTransactionContext } from '../contexts/TransactionContext'; import { transformToBaseUnit } from '../util/evalUnits'; +import { Alert } from '.'; const useStyles = makeStyles(() => ({ container: { @@ -30,7 +31,12 @@ const useStyles = makeStyles(() => ({ export const EstimatedFee = (): React.ReactElement => { const classes = useStyles(); const { sourceChainDetails } = useSourceTarget(); - const { estimatedFee, payloadEstimatedFeeLoading, transactionRunning } = useTransactionContext(); + const { + estimatedFee, + payloadEstimatedFeeLoading, + transactionRunning, + evaluateTransactionStatusError + } = useTransactionContext(); const srcChainDecimals = sourceChainDetails.apiConnection.api.registry.chainDecimals[0]; const { chainTokens } = sourceChainDetails.apiConnection.api.registry; @@ -42,7 +48,9 @@ export const EstimatedFee = (): React.ReactElement => { const feeLabel = `Estimated ${sourceChainDetails.chain} fee`; - return ( + return evaluateTransactionStatusError ? ( + {evaluateTransactionStatusError} + ) : (
{payloadEstimatedFeeLoading && !transactionRunning diff --git a/src/components/Transfer.tsx b/src/components/Transfer.tsx index 01bef27f..ca8f75f9 100644 --- a/src/components/Transfer.tsx +++ b/src/components/Transfer.tsx @@ -14,21 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges UI. If not, see . -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { Box, makeStyles } from '@material-ui/core'; import { useSourceTarget } from '../contexts/SourceTargetContextProvider'; import { useTransactionContext } from '../contexts/TransactionContext'; -import { useAccountContext } from '../contexts/AccountContextProvider'; import { TransactionActionCreators } from '../actions/transactionActions'; import { useUpdateTransactionContext } from '../contexts/TransactionContext'; -import useBalance from '../hooks/subscriptions/useBalance'; import useSendMessage from '../hooks/chain/useSendMessage'; import { TransactionTypes } from '../types/transactionTypes'; import { TokenSymbol } from './TokenSymbol'; import Receiver from './Receiver'; -import { Alert, ButtonSubmit } from '../components'; +import { ButtonSubmit } from '../components'; import { EstimatedFee } from '../components/EstimatedFee'; -import BN from 'bn.js'; import { DebouncedTextField } from './DebouncedTextField'; import { useInternalTransfer } from '../hooks/chain/useInternalTransfer'; import { useGUIContext } from '../contexts/GUIContextProvider'; @@ -55,19 +52,15 @@ const useStyles = makeStyles((theme) => ({ function Transfer() { const { dispatchTransaction } = useUpdateTransactionContext(); const classes = useStyles(); - const [amountNotCorrect, setAmountNotCorrect] = useState(false); const { sourceChainDetails, targetChainDetails } = useSourceTarget(); const { isBridged } = useGUIContext(); - const { account } = useAccountContext(); const { - estimatedFee, transferAmount, transferAmountError, transactionRunning, transactionReadyToExecute } = useTransactionContext(); const { api } = sourceChainDetails.apiConnection; - const balance = useBalance(api, account?.address || ''); const executeInternalTransfer = useInternalTransfer(); const dispatchCallback = useCallback( @@ -99,12 +92,6 @@ function Transfer() { transactionRunning && transferAmount && dispatchCallback(''); }, [dispatchCallback, transactionRunning, transferAmount]); - useEffect((): void => { - estimatedFee && - transferAmount && - setAmountNotCorrect(new BN(balance.free).sub(transferAmount).add(new BN(estimatedFee)).isNeg()); - }, [transferAmount, estimatedFee, balance]); - const buttonLabel = isBridged ? `Send bridge transfer from ${sourceChainDetails.chain} to ${targetChainDetails.chain}` : `Send internal transfer to ${sourceChainDetails.chain}`; @@ -126,16 +113,10 @@ function Transfer() { /> - + {buttonLabel} - {amountNotCorrect ? ( - - Account's amount (including fees: {estimatedFee}) is not enough for this transaction. - - ) : ( - - )} + ); } diff --git a/src/contexts/TransactionContext.tsx b/src/contexts/TransactionContext.tsx index 80c55ddf..3094b31b 100644 --- a/src/contexts/TransactionContext.tsx +++ b/src/contexts/TransactionContext.tsx @@ -26,6 +26,7 @@ import { useGUIContext } from './GUIContextProvider'; import { initTransactionState } from '../reducers/initReducersStates/initTransactionState'; import { useSourceTarget } from './SourceTargetContextProvider'; import { encodeAddress } from '@polkadot/util-crypto'; +import useSenderBalanceUpdates from '../hooks/transactions/useSenderBalanceUpdates'; interface TransactionContextProviderProps { children: React.ReactElement; @@ -51,7 +52,7 @@ export function useUpdateTransactionContext() { export function TransactionContextProvider(props: TransactionContextProviderProps): React.ReactElement { const { children = null } = props; - const { account } = useAccountContext(); + const { account, senderAccountBalance, senderCompanionAccountBalance } = useAccountContext(); const { action } = useGUIContext(); const { sourceChainDetails: { @@ -61,18 +62,17 @@ export function TransactionContextProvider(props: TransactionContextProviderProp const [transactionsState, dispatchTransaction] = useReducer(transactionReducer, initTransactionState); useResetTransactionState(action, dispatchTransaction); - useEstimatedFeePayload(transactionsState, dispatchTransaction); useTransactionsStatus(transactionsState.transactions, transactionsState.evaluatingTransactions, dispatchTransaction); + useSenderBalanceUpdates(senderAccountBalance, senderCompanionAccountBalance, dispatchTransaction); + + useEffect((): void => { + account && dispatchTransaction(TransactionActionCreators.setSender(encodeAddress(account.address, ss58Format))); + }, [account, ss58Format]); useEffect((): void => { - dispatchTransaction( - TransactionActionCreators.setSenderAndAction({ - senderAccount: account ? encodeAddress(account.address, ss58Format) : null, - action - }) - ); - }, [account, action, ss58Format]); + action && dispatchTransaction(TransactionActionCreators.setAction(action)); + }, [action]); return ( diff --git a/src/hooks/transactions/useEstimatedFeePayload.ts b/src/hooks/transactions/useEstimatedFeePayload.ts index b0ff5889..a5df71db 100644 --- a/src/hooks/transactions/useEstimatedFeePayload.ts +++ b/src/hooks/transactions/useEstimatedFeePayload.ts @@ -25,8 +25,13 @@ import type { InterfaceTypes } from '@polkadot/types/types'; import useLaneId from '../chain/useLaneId'; import { getSubstrateDynamicNames } from '../../util/getSubstrateDynamicNames'; import { genericCall } from '../../util/apiUtlis'; -import { PayloadEstimatedFee, TransactionsActionType, TransactionState } from '../../types/transactionTypes'; -import { getTransactionCallWeight } from '../../util/transactions/'; +import { + PayloadEstimatedFee, + TransactionsActionType, + TransactionState, + TransactionTypes +} from '../../types/transactionTypes'; +import { getFeeAndWeightForInternals, getTransactionCallWeight } from '../../util/transactions/'; import { useGUIContext } from '../../contexts/GUIContextProvider'; import usePrevious from '../react/usePrevious'; @@ -41,13 +46,16 @@ export const useEstimatedFeePayload = ( const laneId = useLaneId(); const sourceTargetDetails = useSourceTarget(); const { - sourceChainDetails: { chain: sourceChain }, + sourceChainDetails: { + chain: sourceChain, + apiConnection: { api: sourceApi } + }, targetChainDetails: { apiConnection: { api: targetApi }, chain: targetChain } } = sourceTargetDetails; - const { account } = useAccountContext(); + const { account, senderAccountBalance, senderCompanionAccountBalance } = useAccountContext(); const { action, isBridged } = useGUIContext(); const { estimatedFeeMethodName } = getSubstrateDynamicNames(targetChain); const previousPayloadEstimatedFeeLoading = usePrevious(transactionState.payloadEstimatedFeeLoading); @@ -61,14 +69,36 @@ export const useEstimatedFeePayload = ( loading, sourceTargetDetails, createType, - isBridged + isBridged, + senderAccountBalance, + senderCompanionAccountBalance ) ), - [createType, dispatchTransaction, isBridged, sourceTargetDetails] + [ + createType, + dispatchTransaction, + isBridged, + senderAccountBalance, + senderCompanionAccountBalance, + sourceTargetDetails + ] ); const calculateFeeAndPayload = useCallback( async (currentTransactionState: TransactionState) => { + if (currentTransactionState.action === TransactionTypes.INTERNAL_TRANSFER) { + const { estimatedFee, weight } = await getFeeAndWeightForInternals({ + api: sourceApi, + transactionState: currentTransactionState + }); + const payload = { + sourceAccount: currentTransactionState.senderAccount, + transferAmount: currentTransactionState.transferAmount!.toNumber(), + receiverAddress: currentTransactionState.receiverAddress, + weight + }; + return { estimatedFee, payload }; + } const { call, weight } = await getTransactionCallWeight({ action, account, @@ -103,7 +133,7 @@ export const useEstimatedFeePayload = ( const estimatedFee = estimatedFeeType.toString(); return { estimatedFee, payload }; }, - [account, action, createType, estimatedFeeMethodName, laneId, sourceChain, stateCall, targetApi] + [account, action, createType, estimatedFeeMethodName, laneId, sourceApi, sourceChain, stateCall, targetApi] ); useEffect(() => { @@ -126,7 +156,14 @@ export const useEstimatedFeePayload = ( useEffect(() => { const { batchedTransactionState, payloadEstimatedFeeLoading } = transactionState; - if (previousPayloadEstimatedFeeLoading && !payloadEstimatedFeeLoading && batchedTransactionState) { + + if ( + previousPayloadEstimatedFeeLoading && + !payloadEstimatedFeeLoading && + batchedTransactionState && + senderAccountBalance && + senderCompanionAccountBalance + ) { genericCall({ call: () => calculateFeeAndPayload(batchedTransactionState), dispatch, @@ -140,6 +177,8 @@ export const useEstimatedFeePayload = ( dispatch, dispatchTransaction, previousPayloadEstimatedFeeLoading, + senderAccountBalance, + senderCompanionAccountBalance, transactionState ]); }; diff --git a/src/hooks/transactions/useSenderBalanceUpdates.ts b/src/hooks/transactions/useSenderBalanceUpdates.ts new file mode 100644 index 00000000..d0f38980 --- /dev/null +++ b/src/hooks/transactions/useSenderBalanceUpdates.ts @@ -0,0 +1,55 @@ +// Copyright 2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges UI. +// +// Parity Bridges UI is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Parity Bridges UI is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Parity Bridges UI. If not, see . + +import { Dispatch, useEffect } from 'react'; +import isEqual from 'lodash/isEqual'; +import { TransactionActionCreators } from '../../actions/transactionActions'; + +import { TransactionsActionType } from '../../types/transactionTypes'; +import usePrevious from '../react/usePrevious'; +import { BalanceState } from '../../types/accountTypes'; + +const useSenderBalanceUpdates = ( + senderAccountBalance: BalanceState | null, + senderCompanionAccountBalance: BalanceState | null, + dispatchTransaction: Dispatch +) => { + const prevSenderAccountBalance = usePrevious(senderAccountBalance); + const prevSenderCompanionAccountBalance = usePrevious(senderCompanionAccountBalance); + + useEffect((): void => { + if ( + (senderAccountBalance && + senderCompanionAccountBalance && + !isEqual(prevSenderAccountBalance, senderAccountBalance)) || + !isEqual(prevSenderCompanionAccountBalance, senderCompanionAccountBalance) + ) + dispatchTransaction( + TransactionActionCreators.updateSenderBalances({ + senderAccountBalance, + senderCompanionAccountBalance + }) + ); + }, [ + dispatchTransaction, + prevSenderAccountBalance, + prevSenderCompanionAccountBalance, + senderAccountBalance, + senderCompanionAccountBalance + ]); +}; + +export default useSenderBalanceUpdates; diff --git a/src/reducers/accountReducer.ts b/src/reducers/accountReducer.ts index cb961b65..d6baee7b 100644 --- a/src/reducers/accountReducer.ts +++ b/src/reducers/accountReducer.ts @@ -43,7 +43,7 @@ export default function accountReducer(state: AccountState, action: AccountsActi const companionAccount = getDeriveAccount(toDerive); - return { ...state, account, companionAccount }; + return { ...state, account, companionAccount, senderAccountBalance: null, senderCompanionAccountBalance: null }; } case AccountActionsTypes.SET_SENDER_BALANCES: return { diff --git a/src/reducers/transactionReducer.ts b/src/reducers/transactionReducer.ts index 07545e79..079581d2 100644 --- a/src/reducers/transactionReducer.ts +++ b/src/reducers/transactionReducer.ts @@ -20,7 +20,8 @@ import { updateTransaction, isReadyToExecute, setReceiver, - shouldCalculatePayloadFee + shouldCalculatePayloadFee, + enoughFundsEvaluation } from '../util/transactions/reducer'; import { TransactionsActionType, TransactionState } from '../types/transactionTypes'; import logger from '../util/logger'; @@ -38,18 +39,30 @@ export default function transactionReducer(state: TransactionState, action: Tran payloadEstimatedFeeLoading, sourceTargetDetails, createType, - isBridged + isBridged, + senderAccountBalance, + senderCompanionAccountBalance } = action.payload; - const { senderAccount, transferAmount, receiverAddress } = state; + const { senderAccount, receiverAddress, transferAmount } = state; - const readyToExecute = payloadEstimatedFeeLoading ? false : transactionReadyToExecute; + const { evaluateTransactionStatusError, notEnoughFundsToTransfer, notEnoughToPayFee } = enoughFundsEvaluation({ + transferAmount, + senderCompanionAccountBalance, + senderAccountBalance, + estimatedFee, + action: state.action + }); + + const readyToExecute = payloadEstimatedFeeLoading + ? false + : transactionReadyToExecute && !notEnoughToPayFee && !notEnoughFundsToTransfer; let payloadHex = null; let transactionDisplayPayload = null; - if (senderAccount) { - if (payload && isBridged) { + if (senderAccount && payload) { + if (isBridged) { const updated = getTransactionDisplayPayload({ payload, account: senderAccount, @@ -63,7 +76,8 @@ export default function transactionReducer(state: TransactionState, action: Tran transactionDisplayPayload = { sourceAccount: senderAccount, transferAmount: transferAmount.toNumber(), - receiverAddress: receiverAddress + receiverAddress: receiverAddress, + weight: payload.weight }; } } @@ -74,11 +88,11 @@ export default function transactionReducer(state: TransactionState, action: Tran payloadEstimatedFeeError, payloadEstimatedFeeLoading, payload: payloadEstimatedFeeError ? null : payload, - transactionReadyToExecute: readyToExecute, shouldEvaluatePayloadEstimatedFee: false, payloadHex, - transactionDisplayPayload + transactionDisplayPayload, + evaluateTransactionStatusError }; } @@ -186,6 +200,7 @@ export default function transactionReducer(state: TransactionState, action: Tran case TransactionActionTypes.RESET: return { ...state, + evaluateTransactionStatusError: null, resetedAt: Date.now().toString(), derivedReceiverAccount: null, estimatedFee: null, @@ -228,20 +243,36 @@ export default function transactionReducer(state: TransactionState, action: Tran return setReceiver(state, action.payload.receiverPayload); case TransactionActionTypes.SET_TRANSACTION_RUNNING: return { ...state, transactionRunning: action.payload.transactionRunning, transactionReadyToExecute: false }; - case TransactionActionTypes.SET_SENDER_AND_ACTION: { - const { senderAccount, action: transactionType } = action.payload; + case TransactionActionTypes.SET_ACTION: { + const { action: transactionType } = action.payload; + + return { + ...state, + action: transactionType + }; + } + case TransactionActionTypes.SET_SENDER: { + const { senderAccount } = action.payload; + + return { + ...state, + senderAccount: senderAccount + }; + } + case TransactionActionTypes.UPDATE_SENDER_BALANCES: { + const { action: transactionType, senderAccount } = state; + const shouldEvaluatePayloadEstimatedFee = shouldCalculatePayloadFee(state, { senderAccount, action: transactionType }); + return { ...state, - senderAccount: senderAccount, - action: transactionType, - shouldEvaluatePayloadEstimatedFee + shouldEvaluatePayloadEstimatedFee, + transactionReadyToExecute: false }; } - case TransactionActionTypes.UPDATE_TRANSACTIONS_STATUS: { const { evaluateTransactionStatusError, transactions, evaluatingTransactions } = action.payload; return { @@ -251,6 +282,14 @@ export default function transactionReducer(state: TransactionState, action: Tran evaluateTransactionStatusError }; } + case TransactionActionTypes.SET_TRANSFER_TYPE: { + const { transferType } = action.payload; + return { + ...state, + action: transferType + }; + } + default: throw new Error(`Unknown type: ${action.type}`); } diff --git a/src/screens/Main.tsx b/src/screens/Main.tsx index 0639bce8..db7035ed 100644 --- a/src/screens/Main.tsx +++ b/src/screens/Main.tsx @@ -29,9 +29,10 @@ import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; import Transactions from '../components/Transactions'; import { useGUIContext } from '../contexts/GUIContextProvider'; import { TransactionTypes } from '../types/transactionTypes'; - +import { TransactionActionCreators } from '../actions/transactionActions'; import BridgedLocalWrapper from '../components/BridgedLocalWrapper'; import { useCallback } from 'react'; +import { useUpdateTransactionContext } from '../contexts/TransactionContext'; const useStyles = makeStyles(() => ({ root: { @@ -50,12 +51,18 @@ const ActionComponents = { function Main() { const classes = useStyles(); const { actions, action, setAction, isBridged, setBridged } = useGUIContext(); + const { dispatchTransaction } = useUpdateTransactionContext(); const handleOnSwitch = useCallback( - (event: React.MouseEvent, newAlignment: string | null) => { - setBridged(Boolean(newAlignment)); + (event: React.MouseEvent, isBridged: boolean) => { + setBridged(isBridged); + dispatchTransaction( + TransactionActionCreators.setTransferType( + isBridged ? TransactionTypes.TRANSFER : TransactionTypes.INTERNAL_TRANSFER + ) + ); }, - [setBridged] + [dispatchTransaction, setBridged] ); // TODO #242: ToggleButtonGroup needs to contain the colors designed by custom css. diff --git a/src/types/transactionTypes.ts b/src/types/transactionTypes.ts index f8a8ee38..b5d02a24 100644 --- a/src/types/transactionTypes.ts +++ b/src/types/transactionTypes.ts @@ -40,7 +40,8 @@ export enum SwitchTabEnum { PAYLOAD = 'PAYLOAD', DECODED = 'DECODED' } -export interface TransactionPayload { + +export interface BridgedTransactionPayload { call: Uint8Array; origin: { SourceAccount: Uint8Array; @@ -49,20 +50,28 @@ export interface TransactionPayload { weight: number; } +export interface InternalTransferPayload { + sourceAccount: string | null; + transferAmount: number; + receiverAddress: string | null; + weight: number; +} + +export type TransactionPayload = BridgedTransactionPayload | InternalTransferPayload; + export interface TransactionDisplayPayload { call: Object; origin: Object; spec_version: string; - weight: string; + weight: number; } -export interface LocalTransactionDisplayPayload { - sourceAccount: string; - transferAmount: number; - receiverAddress: string; -} +export type PayloadEstimatedFee = { + payload: TransactionPayload | null; + estimatedFee: string | null; +}; -export type DisplayPayload = TransactionDisplayPayload | LocalTransactionDisplayPayload; +export type DisplayPayload = TransactionDisplayPayload | InternalTransferPayload; export interface TransactionStatusType extends UpdatedTransactionStatusType { input: string; @@ -138,8 +147,3 @@ export interface ReceiverPayload { targetChainDetails: ChainState; isBridged: boolean; } - -export type PayloadEstimatedFee = { - payload: TransactionPayload | null; - estimatedFee: string | null; -}; diff --git a/src/util/transactions/index.ts b/src/util/transactions/index.ts index a15d2e9b..bd88100f 100644 --- a/src/util/transactions/index.ts +++ b/src/util/transactions/index.ts @@ -135,6 +135,20 @@ export async function getTransactionCallWeight({ } return { call, weight }; } + +interface FeeWeightInternal { + api: ApiPromise; + transactionState: TransactionState; +} + +export async function getFeeAndWeightForInternals({ api, transactionState }: FeeWeightInternal) { + const { receiverAddress, transferAmount, senderAccount } = transactionState; + const transfer = api.tx.balances.transfer(receiverAddress!, transferAmount || 0); + + const { partialFee, weight } = await transfer.paymentInfo(senderAccount!); + return { estimatedFee: partialFee.toString(), weight: weight.toNumber() }; +} + const stepEvaluator = (transactionValue: string | number | null, chainValue: string | number | null): boolean => { if (!transactionValue || !chainValue) return false; diff --git a/src/util/transactions/reducer/index.ts b/src/util/transactions/reducer/index.ts index 450a8096..e2aee0cb 100644 --- a/src/util/transactions/reducer/index.ts +++ b/src/util/transactions/reducer/index.ts @@ -21,6 +21,8 @@ import { INCORRECT_FORMAT, GENERIC } from '../../../constants'; import { getValidAddressFormat } from '../../accounts'; import getReceiverAddress from '../../getReceiverAddress'; import logger from '../../logger'; +import BN from 'bn.js'; +import { BalanceState } from '../../../types/accountTypes'; const validateAccount = (receiver: string, sourceChainDetails: ChainState, targetChainDetails: ChainState) => { try { @@ -42,6 +44,51 @@ const validateAccount = (receiver: string, sourceChainDetails: ChainState, targe } }; +interface EnoughFundsEvaluation { + transferAmount: BN | null; + senderAccountBalance: BalanceState; + senderCompanionAccountBalance: BalanceState; + estimatedFee: string | null; + action: TransactionTypes; +} + +const enoughFundsEvaluation = ({ + transferAmount, + senderCompanionAccountBalance, + senderAccountBalance, + estimatedFee, + action +}: EnoughFundsEvaluation) => { + let evaluateTransactionStatusError = null; + let notEnoughFundsToTransfer = false; + let notEnoughToPayFee = false; + + if (senderAccountBalance && estimatedFee) { + notEnoughToPayFee = new BN(senderAccountBalance.free).sub(new BN(estimatedFee)).isNeg(); + if (notEnoughToPayFee) { + evaluateTransactionStatusError = `Account's amount is not enough for pay fee transaction: ${estimatedFee}.`; + } + + if (action === TransactionTypes.TRANSFER && transferAmount && senderCompanionAccountBalance) { + notEnoughFundsToTransfer = new BN(senderCompanionAccountBalance.free).sub(new BN(estimatedFee)).isNeg(); + if (notEnoughFundsToTransfer) { + evaluateTransactionStatusError = "Companion account's amount is not enough for this transaction."; + } + } + + if (action === TransactionTypes.INTERNAL_TRANSFER && transferAmount) { + notEnoughFundsToTransfer = new BN(senderAccountBalance.free) + .sub(transferAmount) + .sub(new BN(estimatedFee)) + .isNeg(); + if (notEnoughFundsToTransfer) { + evaluateTransactionStatusError = "Account's amount is not enough for this transaction."; + } + } + } + return { evaluateTransactionStatusError, notEnoughFundsToTransfer, notEnoughToPayFee }; +}; + const shouldCalculatePayloadFee = (state: TransactionState, payload: Payload) => { const nextState = { ...state, ...payload }; const { @@ -54,7 +101,9 @@ const shouldCalculatePayloadFee = (state: TransactionState, payload: Payload) => senderAccount, action } = nextState; + switch (action) { + case TransactionTypes.INTERNAL_TRANSFER: case TransactionTypes.TRANSFER: { return Boolean(transferAmount && receiverAddress && senderAccount); } @@ -90,6 +139,7 @@ const updateTransaction = (state: TransactionState, payload: Payload): Transacti const isInputReady = (state: TransactionState): boolean => { switch (state.action) { + case TransactionTypes.INTERNAL_TRANSFER: case TransactionTypes.TRANSFER: { return Boolean(state.transferAmount) && Boolean(state.receiverAddress); } @@ -274,4 +324,4 @@ const setReceiver = (state: TransactionState, payload: ReceiverPayload): Transac }; }; -export { updateTransaction, isReadyToExecute, setReceiver, shouldCalculatePayloadFee }; +export { updateTransaction, isReadyToExecute, setReceiver, shouldCalculatePayloadFee, enoughFundsEvaluation };