diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index e8d7419fc5..ff96b76aeb 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -616,6 +616,11 @@ export interface BasicTokenInfo { symbol: string; } +export interface SufficientMetadata { + isSufficient: boolean, + minBalance: number +} + export interface AmountData extends BasicTokenInfo { value: string; metadata?: unknown; diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 9cac5843be..a0234ce650 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -6,12 +6,12 @@ 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'; import { _EvmApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _getChainExistentialDeposit, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isNativeToken, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getChainExistentialDeposit, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isNativeToken, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { isSubstrateTransaction, isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; import { OptionalSWTransaction, SWTransactionInput, SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; @@ -56,37 +56,67 @@ 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[]] { - const minAmount = _getTokenMinAmount(tokenInfo); - const nativeMinAmount = _getTokenMinAmount(nativeTokenInfo); +export function additionalValidateTransferForRecipient ( + sendingTokenInfo: _ChainAsset, + nativeTokenInfo: _ChainAsset, + extrinsicType: ExtrinsicType, + receiverSendingTokenKeepAliveBalance: bigint, + transferAmount: bigint, + senderSendingTokenTransferable?: bigint, + receiverSystemAccountInfo?: FrameSystemAccountInfo, + isSendingTokenSufficient?: boolean +): [TransactionWarning[], TransactionError[]] { + const sendingTokenMinAmount = BigInt(_getTokenMinAmount(sendingTokenInfo)); + const nativeTokenMinAmount = _getTokenMinAmount(nativeTokenInfo); + const warnings: TransactionWarning[] = []; const errors: TransactionError[] = []; - // Check ed of not native token for sender after sending - if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN && senderTransferTokenTransferable) { - if (new BigN(senderTransferTokenTransferable).minus(transferAmount).lt(minAmount)) { + const remainingSendingTokenOfSenderEnoughED = senderSendingTokenTransferable ? senderSendingTokenTransferable - transferAmount >= sendingTokenMinAmount : false; + const isReceiverAliveByNativeToken = _isAccountActive(receiverSystemAccountInfo as FrameSystemAccountInfo); + const isReceivingAmountPassED = receiverSendingTokenKeepAliveBalance + transferAmount >= sendingTokenMinAmount; + + if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN) { + if (!remainingSendingTokenOfSenderEnoughED) { const warning = new TransactionWarning(BasicTxWarningCode.NOT_ENOUGH_EXISTENTIAL_DEPOSIT); warnings.push(warning); } - } - // 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 (!isReceiverAliveByNativeToken && !isSendingTokenSufficient) { + const balanceKeepAlive = formatNumber(nativeTokenMinAmount, _getAssetDecimals(nativeTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(nativeTokenInfo) || 6 }); + + const error = new TransactionError( + TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, + t('The recipient account has less than {{amount}} {{nativeSymbol}}, which can lead to your {{localSymbol}} being lost. Change recipient account and try again', { replace: { amount: balanceKeepAlive, nativeSymbol: nativeTokenInfo.symbol, localSymbol: sendingTokenInfo.symbol } }) + ); + + errors.push(error); + } + + if (!isReceivingAmountPassED) { + const atLeast = sendingTokenMinAmount - receiverSendingTokenKeepAliveBalance; + + const atLeastStr = formatNumber(atLeast.toString(), _getAssetDecimals(sendingTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(sendingTokenInfo) || 6 }); + + const error = new TransactionError( + TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, + t('You must transfer at least {{amount}} {{symbol}} to avoid losing funds on the recipient account. Increase amount and try again', { replace: { amount: atLeastStr, symbol: sendingTokenInfo.symbol } }) + ); 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 (!isReceivingAmountPassED) { + const atLeast = sendingTokenMinAmount - receiverSendingTokenKeepAliveBalance; - const atLeastStr = formatNumber(atLeast, tokenInfo.decimals || 0, balanceFormatter, { maxNumberFormat: tokenInfo.decimals || 6 }); + const atLeastStr = formatNumber(atLeast.toString(), _getAssetDecimals(sendingTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(sendingTokenInfo) || 6 }); - const error = new TransactionError(TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, t('You must transfer at least {{amount}} {{symbol}} to keep the destination account alive', { replace: { amount: atLeastStr, symbol: tokenInfo.symbol } })); + const error = new TransactionError( + TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, + t('You must transfer at least {{amount}} {{symbol}} to keep the recipient account alive. Increase amount and try again', { replace: { amount: atLeastStr, symbol: sendingTokenInfo.symbol } }) + ); errors.push(error); } diff --git a/packages/extension-base/src/core/substrate/system-pallet.ts b/packages/extension-base/src/core/substrate/system-pallet.ts index d1b2c12e1e..0d5fb46685 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 5f986c235f..d769218950 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -7,11 +7,12 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; -import { additionalValidateTransfer, additionalValidateXcmTransfer, validateTransferRequest, validateXcmTransferRequest } from '@subwallet/extension-base/core/logic-validation/transfer'; +import { additionalValidateTransferForRecipient, additionalValidateXcmTransfer, validateTransferRequest, validateXcmTransferRequest } from '@subwallet/extension-base/core/logic-validation/transfer'; +import { FrameSystemAccountInfo } from '@subwallet/extension-base/core/substrate/types'; import { _isSnowBridgeXcm } from '@subwallet/extension-base/core/substrate/xcm-parser'; import { ALLOWED_PATH } from '@subwallet/extension-base/defaults'; import { getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; @@ -33,7 +34,7 @@ import { createTonTransaction } from '@subwallet/extension-base/services/balance import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic, getXcmMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; import { _isPolygonChainBridge, getClaimPolygonBridge, isClaimedPolygonBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; -import { _API_OPTIONS_CHAIN_GROUP, _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX } from '@subwallet/extension-base/services/chain-service/constants'; +import { _API_OPTIONS_CHAIN_GROUP, _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX, SUFFICIENT_CHAIN } from '@subwallet/extension-base/services/chain-service/constants'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _isAssetSmartContractNft, _isChainEvmCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; import { _NotificationInfo, NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; @@ -70,6 +71,7 @@ import { t } from 'i18next'; import { combineLatest, Subject } from 'rxjs'; import { TransactionConfig } from 'web3-core'; +import { ApiPromise } from '@polkadot/api'; import { SubmittableExtrinsic } from '@polkadot/api/types'; import { Metadata, TypeRegistry } from '@polkadot/types'; import { ChainProperties } from '@polkadot/types/interfaces'; @@ -1386,23 +1388,29 @@ export default class KoniExtension { const transferNativeAmount = isTransferNativeToken ? transferAmount.value : '0'; const additionalValidator = async (inputTransaction: SWTransactionResponse): Promise => { - let senderTransferTokenTransferable: string | undefined; - let receiverNativeTransferable: string | undefined; + let senderSendingTokenTransferable: bigint | undefined; + let receiverSystemAccountInfo: FrameSystemAccountInfo | undefined; // Check ed for sender if (!isTransferNativeToken) { - const [_senderTransferTokenTransferable, _receiverNativeTransferable] = await Promise.all([ + const [_senderSendingTokenTransferable, _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; + senderSendingTokenTransferable = BigInt(_senderSendingTokenTransferable.value); + receiverSystemAccountInfo = _receiverNativeTotal.metadata as FrameSystemAccountInfo; } - const { value: receiverTransferTokenTransferable } = await this.getAddressTransferableBalance({ address: to, networkKey, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts + const { value: _receiverSendingTokenKeepAliveBalance } = await this.getAddressTotalBalance({ address: to, networkKey, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts + const receiverSendingTokenKeepAliveBalance = BigInt(_receiverSendingTokenKeepAliveBalance); - const [warnings, errors] = additionalValidateTransfer(transferTokenInfo, nativeTokenInfo, extrinsicType, receiverTransferTokenTransferable, transferAmount.value, senderTransferTokenTransferable, receiverNativeTransferable); + const amount = BigInt(transferAmount.value); + + const substrateApi = this.#koniState.getSubstrateApi(networkKey).api; + const isSufficient = await this.isSufficientToken(transferTokenInfo, substrateApi); + + const [warnings, errors] = additionalValidateTransferForRecipient(transferTokenInfo, nativeTokenInfo, extrinsicType, receiverSendingTokenKeepAliveBalance, amount, senderSendingTokenTransferable, receiverSystemAccountInfo, isSufficient); warnings.length && inputTransaction.warnings.push(...warnings); errors.length && inputTransaction.errors.push(...errors); @@ -1676,6 +1684,22 @@ export default class KoniExtension { } } + private async isSufficientToken (tokenInfo: _ChainAsset, api: ApiPromise): Promise { + let metadata: SufficientMetadata; + + if (SUFFICIENT_CHAIN.includes(tokenInfo.originChain) && tokenInfo.assetType !== _AssetType.NATIVE) { + const assetId = tokenInfo?.metadata?.assetId; + + const _metadata = await api.query.assets.asset(assetId); + + metadata = _metadata.toPrimitive() as unknown as SufficientMetadata; + } else { + return false; + } + + return metadata.isSufficient; + } + private async deleteCustomAsset (assetSlug: string) { const assetInfo = this.#koniState.getAssetBySlug(assetSlug); @@ -1708,6 +1732,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 8394c9139a..2d7703ec73 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,14 @@ 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' | 'keepAlive' = 'transferable', + 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 +226,20 @@ export class BalanceService implements StoppableServiceInterface { unsub = subscribeBalance([address], [chain], [tSlug], assetMap, chainInfoMap, substrateApiMap, evmApiMap, tonApiMap, (result) => { const rs = result[0]; + let value: string; + + switch (balanceType) { + case 'total': + value = new BigN(rs.free).plus(new BigN(rs.locked)).toString(); + break; + default: + value = rs.free; + } + if (rs.tokenSlug === tSlug && rs.state !== APIItemState.PENDING) { hasError = false; const balance: AmountData = { - value: rs.free, + value, decimals: tokenInfo.decimals || 0, symbol: tokenInfo.symbol, metadata: rs.metadata @@ -247,6 +265,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 +290,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/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index fafcfa6608..a8967d518d 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -259,6 +259,7 @@ export const _XCM_CHAIN_GROUP = { xcmPallet: ['polkadot', 'kusama', 'rococo'] // default is xTokens pallet }; +export const SUFFICIENT_CHAIN = ['astar', 'calamari', 'parallel', 'darwinia2', 'crabParachain', 'pangolin', 'statemint', 'moonriver', 'shiden', 'moonbeam', 'statemine', 'liberland', 'dentnet', 'phala', 'crust', 'dbcchain', 'rococo_assethub']; export const _XCM_TYPE = { RP: `${_SubstrateChainType.RELAYCHAIN}-${_SubstrateChainType.PARACHAIN}`, // DMP 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;