diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 78ea7e4f83..f739f8337a 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -6,7 +6,7 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/Tr import { _Address, AmountData, ExtrinsicDataTypeMap, ExtrinsicType, FeeData } from '@subwallet/extension-base/background/KoniTypes'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { LEDGER_SIGNING_COMPATIBLE_MAP, SIGNING_COMPATIBLE_MAP, XCM_MIN_AMOUNT_RATIO } from '@subwallet/extension-base/constants'; -import { _canAccountBeReaped } from '@subwallet/extension-base/core/substrate/system-pallet'; +import { _canAccountBeReaped, _isAccountActive } from '@subwallet/extension-base/core/substrate/system-pallet'; import { FrameSystemAccountInfo } from '@subwallet/extension-base/core/substrate/types'; import { isBounceableAddress } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { _TRANSFER_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; @@ -56,7 +56,7 @@ export function validateTransferRequest (tokenInfo: _ChainAsset, from: _Address, return [errors, keypair, transferValue]; } -export function additionalValidateTransfer (tokenInfo: _ChainAsset, nativeTokenInfo: _ChainAsset, extrinsicType: ExtrinsicType, receiverTransferTokenFreeBalance: string, transferAmount: string, senderTransferTokenTransferable?: string, receiverNativeTransferable?: string): [TransactionWarning[], TransactionError[]] { +export function additionalValidateTransfer (tokenInfo: _ChainAsset, nativeTokenInfo: _ChainAsset, extrinsicType: ExtrinsicType, receiverTransferTokenTotalBalance: string, transferAmount: string, senderTransferTokenTransferable?: string, _receiverNativeTotal?: string, isReceiverActive?: unknown): [TransactionWarning[], TransactionError[]] { const minAmount = _getTokenMinAmount(tokenInfo); const nativeMinAmount = _getTokenMinAmount(nativeTokenInfo); const warnings: TransactionWarning[] = []; @@ -72,17 +72,24 @@ export function additionalValidateTransfer (tokenInfo: _ChainAsset, nativeTokenI } // Check ed for receiver before sending - if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN && receiverNativeTransferable) { - if (new BigN(receiverNativeTransferable).lt(nativeMinAmount)) { - const error = new TransactionError(TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, t('The recipient account has {{amount}} {{nativeSymbol}} which can lead to your {{localSymbol}} being lost. Change recipient account and try again', { replace: { amount: receiverNativeTransferable, nativeSymbol: nativeTokenInfo.symbol, localSymbol: tokenInfo.symbol } })); + if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN && _receiverNativeTotal) { + if (new BigN(_receiverNativeTotal).lt(nativeMinAmount) && new BigN(nativeMinAmount).gt(0)) { + const error = new TransactionError(TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, t('The recipient account has {{amount}} {{nativeSymbol}} which can lead to your {{localSymbol}} being lost. Change recipient account and try again', { replace: { amount: _receiverNativeTotal, nativeSymbol: nativeTokenInfo.symbol, localSymbol: tokenInfo.symbol } })); errors.push(error); } } + // Check if receiver's account is active + if (isReceiverActive && _isAccountActive(isReceiverActive as FrameSystemAccountInfo)) { + const error = new TransactionError(TransferTxErrorType.RECEIVER_ACCOUNT_INACTIVE, t('The recipient account may be inactive. Change recipient account and try again')); + + errors.push(error); + } + // Check ed for receiver after sending - if (new BigN(receiverTransferTokenFreeBalance).plus(transferAmount).lt(minAmount)) { - const atLeast = new BigN(minAmount).minus(receiverTransferTokenFreeBalance).plus((tokenInfo.decimals || 0) === 0 ? 0 : 1); + if (new BigN(receiverTransferTokenTotalBalance).plus(transferAmount).lt(minAmount)) { + const atLeast = new BigN(minAmount).minus(receiverTransferTokenTotalBalance).plus((tokenInfo.decimals || 0) === 0 ? 0 : 1); const atLeastStr = formatNumber(atLeast, tokenInfo.decimals || 0, balanceFormatter, { maxNumberFormat: tokenInfo.decimals || 6 }); diff --git a/packages/extension-base/src/core/substrate/system-pallet.ts b/packages/extension-base/src/core/substrate/system-pallet.ts index d1b2c12e1e..57fa499955 100644 --- a/packages/extension-base/src/core/substrate/system-pallet.ts +++ b/packages/extension-base/src/core/substrate/system-pallet.ts @@ -24,7 +24,7 @@ export function _canAccountBeReaped (accountInfo: FrameSystemAccountInfo): boole } export function _isAccountActive (accountInfo: FrameSystemAccountInfo): boolean { - return accountInfo.providers === 0 && accountInfo.consumers === 0; + return accountInfo.consumers === 0 && accountInfo.providers === 0 && accountInfo.sufficients === 0; } export function _getSystemPalletTotalBalance (accountInfo: FrameSystemAccountInfo): bigint { diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index be6fd11e00..e4806a6632 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -1376,22 +1376,24 @@ export default class KoniExtension { const additionalValidator = async (inputTransaction: SWTransactionResponse): Promise => { let senderTransferTokenTransferable: string | undefined; - let receiverNativeTransferable: string | undefined; + let receiverNativeTotal: string | undefined; + let isReceiverActive: unknown; // Check ed for sender if (!isTransferNativeToken) { - const [_senderTransferTokenTransferable, _receiverNativeTransferable] = await Promise.all([ + const [_senderTransferTokenTransferable, _receiverNativeTotal] = await Promise.all([ this.getAddressTransferableBalance({ address: from, networkKey, token: tokenSlug, extrinsicType }), - this.getAddressTransferableBalance({ address: to, networkKey, token: nativeTokenSlug, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }) + this.getAddressTotalBalance({ address: to, networkKey, token: nativeTokenSlug, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }) ]); senderTransferTokenTransferable = _senderTransferTokenTransferable.value; - receiverNativeTransferable = _receiverNativeTransferable.value; + receiverNativeTotal = _receiverNativeTotal.value; + isReceiverActive = _receiverNativeTotal.metadata; } - const { value: receiverTransferTokenTransferable } = await this.getAddressTransferableBalance({ address: to, networkKey, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts + const { value: receiverTransferTokenTransferable } = await this.getAddressTotalBalance({ address: to, networkKey, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts - const [warnings, errors] = additionalValidateTransfer(transferTokenInfo, nativeTokenInfo, extrinsicType, receiverTransferTokenTransferable, transferAmount.value, senderTransferTokenTransferable, receiverNativeTransferable); + const [warnings, errors] = additionalValidateTransfer(transferTokenInfo, nativeTokenInfo, extrinsicType, receiverTransferTokenTransferable, transferAmount.value, senderTransferTokenTransferable, receiverNativeTotal, isReceiverActive); warnings.length && inputTransaction.warnings.push(...warnings); errors.length && inputTransaction.errors.push(...errors); @@ -1652,6 +1654,10 @@ export default class KoniExtension { return await this.#koniState.balanceService.getTransferableBalance(address, networkKey, token, extrinsicType); } + private async getAddressTotalBalance ({ address, extrinsicType, networkKey, token }: RequestFreeBalance): Promise { + return await this.#koniState.balanceService.getTotalBalance(address, networkKey, token, extrinsicType); + } + private async getMaxTransferable ({ address, destChain, isXcmTransfer, networkKey, token }: RequestMaxTransferable): Promise { const tokenInfo = token ? this.#koniState.chainService.getAssetBySlug(token) : this.#koniState.chainService.getNativeTokenInfo(networkKey); diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index df03477d33..7e8bda4c4b 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -17,6 +17,7 @@ import { addLazy, createPromiseHandler, isAccountAll, PromiseHandler, waitTimeou import { getKeypairTypeByAddress } from '@subwallet/keyring'; import { EthereumKeypairTypes, SubstrateKeypairTypes } from '@subwallet/keyring/types'; import keyring from '@subwallet/ui-keyring'; +import BigN from 'bignumber.js'; import { t } from 'i18next'; import { BehaviorSubject } from 'rxjs'; @@ -189,7 +190,7 @@ export class BalanceService implements StoppableServiceInterface { } /** Subscribe token free balance of an address on chain */ - public async subscribeTransferableBalance (address: string, chain: string, tokenSlug: string | undefined, extrinsicType?: ExtrinsicType, callback?: (rs: AmountData) => void): Promise<[() => void, AmountData]> { + public async subscribeBalance (address: string, chain: string, tokenSlug: string | undefined, balanceType: 'transferable' | 'total', extrinsicType?: ExtrinsicType, callback?: (rs: AmountData) => void): Promise<[() => void, AmountData]> { const chainInfo = this.state.chainService.getChainInfoByKey(chain); const chainState = this.state.chainService.getChainStateByKey(chain); @@ -218,10 +219,14 @@ export class BalanceService implements StoppableServiceInterface { unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => { const rs = result[0]; + const value = balanceType === 'total' + ? new BigN(rs.free).plus(new BigN(rs.locked)).toString() + : rs.free; + if (rs.tokenSlug === tSlug) { hasError = false; const balance: AmountData = { - value: rs.free, + value, decimals: tokenInfo.decimals || 0, symbol: tokenInfo.symbol, metadata: rs.metadata @@ -247,6 +252,14 @@ export class BalanceService implements StoppableServiceInterface { }); } + public async subscribeTransferableBalance (address: string, chain: string, tokenSlug: string | undefined, extrinsicType?: ExtrinsicType, callback?: (rs: AmountData) => void): Promise<[() => void, AmountData]> { + return this.subscribeBalance(address, chain, tokenSlug, 'transferable', extrinsicType, callback); + } + + public async subscribeTotalBalance (address: string, chain: string, tokenSlug: string | undefined, extrinsicType?: ExtrinsicType, callback?: (rs: AmountData) => void): Promise<[() => void, AmountData]> { + return this.subscribeBalance(address, chain, tokenSlug, 'total', extrinsicType, callback); + } + /** * @public * @async @@ -264,6 +277,12 @@ export class BalanceService implements StoppableServiceInterface { return balance; } + public async getTotalBalance (address: string, chain: string, tokenSlug?: string, extrinsicType?: ExtrinsicType): Promise { + const [, balance] = await this.subscribeTotalBalance(address, chain, tokenSlug, extrinsicType); + + return balance; + } + /** Remove balance from the subject object by addresses */ public removeBalanceByAddresses (addresses: string[]) { this.balanceMap.removeBalanceItems([...addresses, ALL_ACCOUNT_KEY]); diff --git a/packages/extension-base/src/types/transaction/error.ts b/packages/extension-base/src/types/transaction/error.ts index a0c00d6101..d86b8f1794 100644 --- a/packages/extension-base/src/types/transaction/error.ts +++ b/packages/extension-base/src/types/transaction/error.ts @@ -42,6 +42,7 @@ export enum TransferTxErrorType { INVALID_TOKEN = 'INVALID_TOKEN', TRANSFER_ERROR = 'TRANSFER_ERROR', RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT = 'RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT', + RECEIVER_ACCOUNT_INACTIVE = 'RECEIVER_ACCOUNT_INACTIVE' } export type TransactionErrorType = BasicTxErrorType | TransferTxErrorType | StakingTxErrorType | YieldValidationStatus | SwapErrorType;