diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 57be57f2cd..68a679188b 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -11,7 +11,7 @@ import { FrameSystemAccountInfo } from '@subwallet/extension-base/core/substrate 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 { _getAssetDecimals, _getChainExistentialDeposit, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getTokenMinAmount, _isNativeToken, _isTokenEvmSmartContract, _isTokenTonSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _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'; @@ -67,6 +67,7 @@ export function additionalValidateTransferForRecipient ( isSendingTokenSufficient?: boolean ): [TransactionWarning[], TransactionError[]] { const sendingTokenMinAmount = BigInt(_getTokenMinAmount(sendingTokenInfo)); + const sendingTokenMinAmountXCM = new BigN(_getTokenMinAmount(sendingTokenInfo)).multipliedBy(XCM_MIN_AMOUNT_RATIO); const nativeTokenMinAmount = _getTokenMinAmount(nativeTokenInfo); const warnings: TransactionWarning[] = []; @@ -75,8 +76,21 @@ export function additionalValidateTransferForRecipient ( const remainingSendingTokenOfSenderEnoughED = senderSendingTokenTransferable ? senderSendingTokenTransferable - transferAmount >= sendingTokenMinAmount : false; const isReceiverAliveByNativeToken = receiverSystemAccountInfo ? _isAccountActive(receiverSystemAccountInfo) : false; const isReceivingAmountPassED = receiverSendingTokenKeepAliveBalance + transferAmount >= sendingTokenMinAmount; + const enoughAmountForXCM = extrinsicType === ExtrinsicType.TRANSFER_XCM ? new BigN(transferAmount.toString()).gte(sendingTokenMinAmountXCM) : true; + const isReceivingNonNativeToken = extrinsicType === ExtrinsicType.TRANSFER_TOKEN || (extrinsicType === ExtrinsicType.TRANSFER_XCM && !_isNativeToken(sendingTokenInfo)); - if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN) { + if (!enoughAmountForXCM) { + const minXCMStr = formatNumber(sendingTokenMinAmountXCM.toString(), _getAssetDecimals(sendingTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(sendingTokenInfo) || 6 }); + + const error = new TransactionError( + TransferTxErrorType.NOT_ENOUGH_VALUE, + t('You must transfer at least {{amount}} {{symbol}} to avoid losing funds on the recipient account. Increase amount and try again', { replace: { amount: minXCMStr, symbol: sendingTokenInfo.symbol } }) + ); + + errors.push(error); + } + + if (isReceivingNonNativeToken) { if (!remainingSendingTokenOfSenderEnoughED) { const warning = new TransactionWarning(BasicTxWarningCode.NOT_ENOUGH_EXISTENTIAL_DEPOSIT); @@ -137,40 +151,6 @@ export function validateXcmTransferRequest (destTokenInfo: _ChainAsset | undefin return [errors, keypair, transferValue]; } -export function additionalValidateXcmTransfer (originTokenInfo: _ChainAsset, destinationTokenInfo: _ChainAsset, sendingAmount: string, senderTransferable: string, receiverNativeBalance: string, destChainInfo: _ChainInfo, isSnowBridge = false): [TransactionWarning | undefined, TransactionError | undefined] { - const destMinAmount = _getTokenMinAmount(destinationTokenInfo); - const minSendingRequired = new BigN(destMinAmount).multipliedBy(XCM_MIN_AMOUNT_RATIO); - - let error: TransactionError | undefined; - let warning: TransactionWarning | undefined; - - // Check sending token ED for receiver - if (new BigN(sendingAmount).lt(minSendingRequired)) { - const atLeastStr = formatNumber(minSendingRequired, destinationTokenInfo.decimals || 0, balanceFormatter, { maxNumberFormat: destinationTokenInfo.decimals || 6 }); - - 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: originTokenInfo.symbol } })); - } - - // check native token ED on dest chain for receiver - const bnKeepAliveBalance = _isNativeToken(destinationTokenInfo) ? new BigN(receiverNativeBalance).plus(sendingAmount) : new BigN(receiverNativeBalance); - - if (isSnowBridge && bnKeepAliveBalance.lt(_getChainExistentialDeposit(destChainInfo))) { - const { decimals, symbol } = _getChainNativeTokenBasicInfo(destChainInfo); - const atLeastStr = formatNumber(_getChainExistentialDeposit(destChainInfo), decimals || 0, balanceFormatter, { maxNumberFormat: 6 }); - - error = new TransactionError(TransferTxErrorType.RECEIVER_NOT_ENOUGH_EXISTENTIAL_DEPOSIT, t(' Insufficient {{symbol}} on {{chain}} to cover min balance ({{amount}} {{symbol}})', { replace: { amount: atLeastStr, symbol, chain: destChainInfo.name } })); - } - - // Check ed for sender - if (!_isNativeToken(originTokenInfo)) { - if (new BigN(senderTransferable).minus(sendingAmount).lt(_getTokenMinAmount(originTokenInfo))) { - warning = new TransactionWarning(BasicTxWarningCode.NOT_ENOUGH_EXISTENTIAL_DEPOSIT); - } - } - - return [warning, error]; -} - export function checkSupportForFeature (validationResponse: SWTransactionResponse, blockedFeaturesList: string[], chainInfo: _ChainInfo) { const extrinsicType = validationResponse.extrinsicType; const chain = validationResponse.chain; diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index ad8c22d398..2d0c5ff5cb 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -12,7 +12,7 @@ import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, As 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 { additionalValidateTransferForRecipient, additionalValidateXcmTransfer, validateTransferRequest, validateXcmTransferRequest } from '@subwallet/extension-base/core/logic-validation/transfer'; +import { additionalValidateTransferForRecipient, 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'; @@ -38,7 +38,7 @@ import { _isPolygonChainBridge, getClaimPolygonBridge, isClaimedPolygonBridge } import { _isPosChainBridge, getClaimPosBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; 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, _SubstrateAdapterQueryArgs, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; -import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _getTokenMinAmount, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getChainNativeTokenSlug, _getContractAddressOfToken, _getEvmChainId, _getTokenMinAmount, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; import { ClaimPolygonBridgeNotificationMetadata, NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; @@ -1452,6 +1452,10 @@ export default class KoniExtension { const originTokenInfo = this.#koniState.getAssetBySlug(tokenSlug); const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destinationNetworkKey, tokenSlug); + + const destinationNativeTokenInfo = this.#koniState.getNativeTokenInfo(destinationNetworkKey); + const destinationNativeTokenSlug: string = destinationNativeTokenInfo.slug; + const [errors, fromKeyPair] = validateXcmTransferRequest(destinationTokenInfo, from, value); let extrinsic: SubmittableExtrinsic<'promise'> | TransactionConfig | null = null; @@ -1465,6 +1469,7 @@ export default class KoniExtension { const isSnowBridgeEvmTransfer = _isPureEvmChain(chainInfoMap[originNetworkKey]) && _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]) && !isAvailBridgeFromEvm; const isPolygonBridgeTransfer = _isPolygonChainBridge(originNetworkKey, destinationNetworkKey); const isPosBridgeTransfer = _isPosChainBridge(originNetworkKey, destinationNetworkKey); + const extrinsicType = ExtrinsicType.TRANSFER_XCM; let additionalValidator: undefined | ((inputTransaction: SWTransactionResponse) => Promise); let eventsHandler: undefined | ((eventEmitter: TransactionEmitter) => void); @@ -1500,20 +1505,50 @@ export default class KoniExtension { extrinsic = await funcCreateExtrinsic(params); additionalValidator = async (inputTransaction: SWTransactionResponse): Promise => { - const { value: senderTransferable } = await this.getAddressTransferableBalance({ address: from, networkKey: originNetworkKey, token: originTokenInfo.slug }); - const isSnowBridge = _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]); - let recipientNativeBalance = '0'; + let isSendingTokenSufficient = false; + let receiverSystemAccountInfo: FrameSystemAccountInfo | undefined; + + const { value: _senderTransferable } = await this.getAddressTransferableBalance({ address: from, networkKey: originNetworkKey, token: originTokenInfo.slug }); + const senderTransferable = BigInt(_senderTransferable); + + // const isSnowBridge = _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]); + + const sendingAmount = BigInt(value); + + const { value: _receiverDestinationTokenKeepAliveBalance } = await this.getAddressTotalBalance({ address: to, networkKey: destinationNetworkKey, token: destinationTokenInfo.slug, extrinsicType }); + const receiverDestinationTokenKeepAliveBalance = BigInt(_receiverDestinationTokenKeepAliveBalance); - if (isSnowBridge) { - const { value } = await this.getAddressTransferableBalance({ address: to, networkKey: destinationNetworkKey, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }); + // if (isSnowBridge) { + // const { value } = await this.getAddressTransferableBalance({ address: to, networkKey: destinationNetworkKey, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }); + // + // receiverNativeBalance = BigInt(value); + // } - recipientNativeBalance = value; + if (!_isNativeToken(destinationTokenInfo)) { + const _receiverNativeTotal = await this.getAddressTotalBalance({ address: to, networkKey: destinationNetworkKey, token: destinationNativeTokenSlug, extrinsicType }); + + receiverSystemAccountInfo = _receiverNativeTotal.metadata as FrameSystemAccountInfo; } - const [warning, error] = additionalValidateXcmTransfer(originTokenInfo, destinationTokenInfo, value, senderTransferable, recipientNativeBalance, chainInfoMap[destinationNetworkKey], isSnowBridge); + if (_isChainSubstrateCompatible(chainInfoMap[destinationNetworkKey])) { + const substrateApi = this.#koniState.getSubstrateApi(destinationNetworkKey); + + isSendingTokenSufficient = await this.isSufficientToken(destinationTokenInfo, substrateApi); + } - error && inputTransaction.errors.push(error); - warning && inputTransaction.warnings.push(warning); + const [warning, error] = additionalValidateTransferForRecipient( + destinationTokenInfo, + destinationNativeTokenInfo, + extrinsicType, + receiverDestinationTokenKeepAliveBalance, + sendingAmount, + senderTransferable, // different from sendingTokenInfo being passed in + receiverSystemAccountInfo, + isSendingTokenSufficient + ); + + warning.length && inputTransaction.warnings.push(...warning); + error.length && inputTransaction.errors.push(...error); }; eventsHandler = (eventEmitter: TransactionEmitter) => { @@ -1550,7 +1585,7 @@ export default class KoniExtension { chain: originNetworkKey, transaction: extrinsic, data: inputData, - extrinsicType: ExtrinsicType.TRANSFER_XCM, + extrinsicType, chainType: !isSnowBridgeEvmTransfer && !isAvailBridgeFromEvm && !isPolygonBridgeTransfer && !isPosBridgeTransfer ? ChainType.SUBSTRATE : ChainType.EVM, transferNativeAmount: _isNativeToken(originTokenInfo) ? value : '0', ignoreWarnings, @@ -1706,6 +1741,11 @@ export default class KoniExtension { queryParams.module = 'assets'; } + if (tokenInfo.originChain === 'hydradx_main') { + queryParams.module = 'assetRegistry'; + queryParams.method = 'assets'; + } + metadata = (await substrateApi.makeRpcQuery(queryParams)) as unknown as SufficientMetadata; } else { return false; diff --git a/packages/extension-base/src/services/chain-service/constants.ts b/packages/extension-base/src/services/chain-service/constants.ts index c2b181f5a3..652bbcd63b 100644 --- a/packages/extension-base/src/services/chain-service/constants.ts +++ b/packages/extension-base/src/services/chain-service/constants.ts @@ -260,7 +260,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 SUFFICIENT_CHAIN = ['astar', 'calamari', 'parallel', 'darwinia2', 'crabParachain', 'pangolin', 'statemint', 'moonriver', 'shiden', 'moonbeam', 'statemine', 'liberland', 'dentnet', 'phala', 'crust', 'dbcchain', 'rococo_assethub', 'hydradx_main']; export const _XCM_TYPE = { RP: `${_SubstrateChainType.RELAYCHAIN}-${_SubstrateChainType.PARACHAIN}`, // DMP