diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 57be57f2cd..243f4f1deb 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,16 +67,19 @@ export function additionalValidateTransferForRecipient ( isSendingTokenSufficient?: boolean ): [TransactionWarning[], TransactionError[]] { const sendingTokenMinAmount = BigInt(_getTokenMinAmount(sendingTokenInfo)); + const sendingTokenMinAmountXCM = BigInt(new BigN(_getTokenMinAmount(sendingTokenInfo)).multipliedBy(XCM_MIN_AMOUNT_RATIO).toString()); const nativeTokenMinAmount = _getTokenMinAmount(nativeTokenInfo); + const minSendingRequired = extrinsicType !== ExtrinsicType.TRANSFER_XCM ? sendingTokenMinAmount : sendingTokenMinAmountXCM; const warnings: TransactionWarning[] = []; const errors: TransactionError[] = []; const remainingSendingTokenOfSenderEnoughED = senderSendingTokenTransferable ? senderSendingTokenTransferable - transferAmount >= sendingTokenMinAmount : false; const isReceiverAliveByNativeToken = receiverSystemAccountInfo ? _isAccountActive(receiverSystemAccountInfo) : false; - const isReceivingAmountPassED = receiverSendingTokenKeepAliveBalance + transferAmount >= sendingTokenMinAmount; + const isReceivingAmountPassED = receiverSendingTokenKeepAliveBalance + transferAmount >= minSendingRequired; + const isReceivingNonNativeToken = extrinsicType === ExtrinsicType.TRANSFER_TOKEN || (extrinsicType === ExtrinsicType.TRANSFER_XCM && !_isNativeToken(sendingTokenInfo)); - if (extrinsicType === ExtrinsicType.TRANSFER_TOKEN) { + if (isReceivingNonNativeToken) { if (!remainingSendingTokenOfSenderEnoughED) { const warning = new TransactionWarning(BasicTxWarningCode.NOT_ENOUGH_EXISTENTIAL_DEPOSIT); @@ -98,10 +101,11 @@ export function additionalValidateTransferForRecipient ( const atLeast = sendingTokenMinAmount - receiverSendingTokenKeepAliveBalance; const atLeastStr = formatNumber(atLeast.toString(), _getAssetDecimals(sendingTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(sendingTokenInfo) || 6 }); + const atLeastAmountStr = extrinsicType === ExtrinsicType.TRANSFER_XCM ? (Number(atLeastStr) * XCM_MIN_AMOUNT_RATIO).toString() : atLeastStr; 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 } }) + t('You must transfer at least {{amount}} {{symbol}} to avoid losing funds on the recipient account. Increase amount and try again', { replace: { amount: atLeastAmountStr, symbol: sendingTokenInfo.symbol } }) ); errors.push(error); @@ -112,10 +116,11 @@ export function additionalValidateTransferForRecipient ( const atLeast = sendingTokenMinAmount - receiverSendingTokenKeepAliveBalance; const atLeastStr = formatNumber(atLeast.toString(), _getAssetDecimals(sendingTokenInfo), balanceFormatter, { maxNumberFormat: _getAssetDecimals(sendingTokenInfo) || 6 }); + const atLeastAmountStr = extrinsicType === ExtrinsicType.TRANSFER_XCM ? (Number(atLeastStr) * XCM_MIN_AMOUNT_RATIO).toString() : atLeastStr; 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 } }) + t('You must transfer at least {{amount}} {{symbol}} to keep the recipient account alive. Increase amount and try again', { replace: { amount: atLeastAmountStr, symbol: sendingTokenInfo.symbol } }) ); errors.push(error); @@ -137,40 +142,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 7a33f1698c..48ecfe90ba 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -11,7 +11,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'; @@ -36,7 +36,7 @@ import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailChainBridge } from '@su 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, 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, _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, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _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'; 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'; @@ -1450,6 +1450,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; @@ -1462,6 +1466,7 @@ export default class KoniExtension { const isAvailBridgeFromAvail = isAvailChainBridge(originNetworkKey) && _isPureEvmChain(chainInfoMap[destinationNetworkKey]); const isSnowBridgeEvmTransfer = _isPureEvmChain(chainInfoMap[originNetworkKey]) && _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]) && !isAvailBridgeFromEvm; const isPolygonBridgeTransfer = _isPolygonChainBridge(originNetworkKey, destinationNetworkKey); + const extrinsicType = ExtrinsicType.TRANSFER_XCM; let additionalValidator: undefined | ((inputTransaction: SWTransactionResponse) => Promise); let eventsHandler: undefined | ((eventEmitter: TransactionEmitter) => void); @@ -1497,20 +1502,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); - if (isSnowBridge) { - const { value } = await this.getAddressTransferableBalance({ address: to, networkKey: destinationNetworkKey, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }); + const { value: _receiverDestinationTokenKeepAliveBalance } = await this.getAddressTotalBalance({ address: to, networkKey: destinationNetworkKey, token: destinationTokenInfo.slug, extrinsicType }); + const receiverDestinationTokenKeepAliveBalance = BigInt(_receiverDestinationTokenKeepAliveBalance); - recipientNativeBalance = value; + // if (isSnowBridge) { + // const { value } = await this.getAddressTransferableBalance({ address: to, networkKey: destinationNetworkKey, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }); + // + // receiverNativeBalance = BigInt(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); - error && inputTransaction.errors.push(error); - warning && inputTransaction.warnings.push(warning); + isSendingTokenSufficient = await this.isSufficientToken(destinationTokenInfo, substrateApi); + } + + 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) => { @@ -1547,7 +1582,7 @@ export default class KoniExtension { chain: originNetworkKey, transaction: extrinsic, data: inputData, - extrinsicType: ExtrinsicType.TRANSFER_XCM, + extrinsicType, chainType: !isSnowBridgeEvmTransfer && !isAvailBridgeFromEvm && !isPolygonBridgeTransfer ? ChainType.SUBSTRATE : ChainType.EVM, transferNativeAmount: _isNativeToken(originTokenInfo) ? value : '0', ignoreWarnings,