diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 20a131b6b51..e707676d592 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -10,13 +10,15 @@ import { RequestOptimalTransferProcess } from '@subwallet/extension-base/service import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { _CHAIN_VALIDATION_ERROR } from '@subwallet/extension-base/services/chain-service/handler/types'; import { _ChainState, _EvmApi, _NetworkUpsertParams, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; +import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; 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 { AuthUrls } from '@subwallet/extension-base/services/request-service/types'; import { CrowdloanContributionsResponse } from '@subwallet/extension-base/services/subscan-service/types'; import { SWTransactionResponse, SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; -import { AccountJson, AccountsWithCurrentAddress, AddressJson, BalanceJson, BaseRequestSign, BuyServiceInfo, BuyTokenInfo, CommonOptimalPath, CurrentAccountInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, InternalRequestSign, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestAccountProxyEdit, RequestAccountProxyForget, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckCrossChainTransfer, RequestCheckPublicAndSecretKey, RequestCheckTransfer, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseEarlyValidateYield, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StorageDataInterface, SubmitYieldStepData, SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, SwapTxData, TokenSpendingApprovalParams, UnlockDotTransactionNft, UnstakingStatus, ValidateSwapProcessParams, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo } from '@subwallet/extension-base/types'; +import { AccountJson, AccountsWithCurrentAddress, AddressJson, BalanceJson, BaseRequestSign, BuyServiceInfo, BuyTokenInfo, CommonOptimalPath, CurrentAccountInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, InternalRequestSign, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestAccountProxyEdit, RequestAccountProxyForget, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckCrossChainTransfer, RequestCheckPublicAndSecretKey, RequestCheckTransfer, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseEarlyValidateYield, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StorageDataInterface, SubmitYieldStepData, SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, SwapTxData, TokenSpendingApprovalParams, UnlockDotTransactionNft, UnstakingStatus, ValidateSwapProcessParams, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo } from '@subwallet/extension-base/types'; +import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { RequestClaimBridge } from '@subwallet/extension-base/types/bridge'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; import { InjectedAccount, InjectedAccountWithMeta, MetadataDefBase } from '@subwallet/extension-inject/types'; @@ -2158,19 +2160,23 @@ export interface KoniRequestSignatures { 'pri(transaction.history.getSubscription)': [null, TransactionHistoryItem[], TransactionHistoryItem[]]; 'pri(transaction.history.subscribe)': [RequestSubscribeHistory, ResponseSubscribeHistory, TransactionHistoryItem[]]; 'pri(transfer.getMaxTransferable)': [RequestMaxTransferable, AmountData]; + 'pri(transfer.subscribe)': [RequestSubscribeTransfer, ResponseSubscribeTransfer, ResponseSubscribeTransfer]; 'pri(subscription.cancel)': [string, boolean]; 'pri(freeBalance.get)': [RequestFreeBalance, AmountData]; 'pri(freeBalance.subscribe)': [RequestFreeBalance, AmountDataWithId, AmountDataWithId]; // Transfer 'pri(accounts.checkTransfer)': [RequestCheckTransfer, ValidateTransactionResponse]; - 'pri(accounts.transfer)': [RequestTransfer, SWTransactionResponse]; + 'pri(accounts.transfer)': [RequestSubmitTransfer, SWTransactionResponse]; 'pri(accounts.getOptimalTransferProcess)': [RequestOptimalTransferProcess, CommonOptimalPath]; 'pri(accounts.approveSpending)': [TokenSpendingApprovalParams, SWTransactionResponse]; 'pri(accounts.checkCrossChainTransfer)': [RequestCheckCrossChainTransfer, ValidateTransactionResponse]; 'pri(accounts.crossChainTransfer)': [RequestCrossChainTransfer, SWTransactionResponse]; + 'pri(customFee.getTokensCanPayFee)': [RequestGetTokensCanPayFee, TokenHasBalanceInfo[]]; + 'pri(customFee.getAmountForPair)': [RequestGetAmountForPair, string]; + // Confirmation Queues 'pri(confirmations.subscribe)': [RequestConfirmationsSubscribe, ConfirmationsQueue, ConfirmationsQueue]; 'pri(confirmationsTon.subscribe)': [RequestConfirmationsSubscribeTon, ConfirmationsQueueTon, ConfirmationsQueueTon]; diff --git a/packages/extension-base/src/core/logic-validation/request.ts b/packages/extension-base/src/core/logic-validation/request.ts index a88911f93a4..bc34b70adae 100644 --- a/packages/extension-base/src/core/logic-validation/request.ts +++ b/packages/extension-base/src/core/logic-validation/request.ts @@ -6,11 +6,11 @@ import { EvmProviderError } from '@subwallet/extension-base/background/errors/Ev import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { ConfirmationType, ErrorValidation, EvmProviderErrorType, EvmSendTransactionParams, EvmSignatureRequest, EvmTransactionData } from '@subwallet/extension-base/background/KoniTypes'; import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { AuthUrlInfo } from '@subwallet/extension-base/services/request-service/types'; -import { BasicTxErrorType } from '@subwallet/extension-base/types'; -import { BN_ZERO, createPromiseHandler, isSameAddress, stripUrl, wait } from '@subwallet/extension-base/utils'; +import { BasicTxErrorType, EvmFeeInfo } from '@subwallet/extension-base/types'; +import { BN_ZERO, combineEthFee, createPromiseHandler, isSameAddress, stripUrl, wait } from '@subwallet/extension-base/utils'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { isSubstrateAddress } from '@subwallet/keyring'; import { KeyringPair } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; @@ -434,18 +434,23 @@ export async function validationEvmDataTransactionMiddleware (koni: KoniState, u estimateGas = new BigN(transactionParams.gasPrice).multipliedBy(transaction.gas).toFixed(0); } else { try { - const priority = await calculateGasFeeParams(evmApi, networkKey || ''); - - if (priority.baseGasFee) { - transaction.maxPriorityFeePerGas = priority.maxPriorityFeePerGas.toString(); - transaction.maxFeePerGas = priority.maxFeePerGas.toString(); - - const maxFee = priority.maxFeePerGas; - - estimateGas = maxFee.multipliedBy(transaction.gas).toFixed(0); + const gasLimit = transaction.gas || await evmApi.api.eth.estimateGas(transaction); + const id = getId(); + const feeInfo = await koni.feeService.subscribeChainFee(id, transaction.chain || '', 'evm') as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo); + + if (transaction.maxFeePerGas) { + estimateGas = new BigN(transaction.maxFeePerGas.toString()).multipliedBy(gasLimit).toFixed(0); + } else if (transaction.gasPrice) { + estimateGas = new BigN(transaction.gasPrice.toString()).multipliedBy(gasLimit).toFixed(0); } else { - transaction.gasPrice = priority.gasPrice; - estimateGas = new BigN(priority.gasPrice).multipliedBy(transaction.gas).toFixed(0); + if (feeCombine.maxFeePerGas) { + const maxFee = new BigN(feeCombine.maxFeePerGas); // TODO: Need review + + estimateGas = maxFee.multipliedBy(gasLimit).toFixed(0); + } else if (feeCombine.gasPrice) { + estimateGas = new BigN((feeCombine.gasPrice || 0)).multipliedBy(gasLimit).toFixed(0); + } } } catch (e) { handleError((e as Error).message); diff --git a/packages/extension-base/src/core/logic-validation/transfer.ts b/packages/extension-base/src/core/logic-validation/transfer.ts index 57be57f2cdc..8f352aaf7fa 100644 --- a/packages/extension-base/src/core/logic-validation/transfer.ts +++ b/packages/extension-base/src/core/logic-validation/transfer.ts @@ -10,13 +10,13 @@ import { _canAccountBeReaped, _isAccountActive } from '@subwallet/extension-base 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 { _EvmApi, _SubstrateApi, _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 { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; +import { calculateToAmountByReservePool, FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE } 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'; -import { AccountSignMode, BasicTxErrorType, BasicTxWarningCode, TransferTxErrorType } from '@subwallet/extension-base/types'; -import { balanceFormatter, formatNumber, pairToAccount } from '@subwallet/extension-base/utils'; +import { AccountSignMode, BasicTxErrorType, BasicTxWarningCode, EvmEIP1559FeeOption, EvmFeeInfo, TransferTxErrorType } from '@subwallet/extension-base/types'; +import { balanceFormatter, combineEthFee, formatNumber, pairToAccount } from '@subwallet/extension-base/utils'; import { isTonAddress } from '@subwallet/keyring'; import { KeyringPair } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; @@ -26,19 +26,13 @@ import { t } from 'i18next'; import { isEthereumAddress } from '@polkadot/util-crypto'; // normal transfer -export function validateTransferRequest (tokenInfo: _ChainAsset, from: _Address, to: _Address, value: string | undefined, transferAll: boolean | undefined): [TransactionError[], KeyringPair | undefined, BigN | undefined] { +export function validateTransferRequest (tokenInfo: _ChainAsset, from: _Address, to: _Address, value: string | undefined, transferAll: boolean | undefined): TransactionError[] { const errors: TransactionError[] = []; - const keypair = keyring.getPair(from); - let transferValue; if (!transferAll) { if (value === undefined) { errors.push(new TransactionError(BasicTxErrorType.INVALID_PARAMS, t('Transfer amount is required'))); } - - if (value) { - transferValue = new BigN(value); - } } if (!tokenInfo) { @@ -53,7 +47,7 @@ export function validateTransferRequest (tokenInfo: _ChainAsset, from: _Address, errors.push(new TransactionError(BasicTxErrorType.INVALID_PARAMS, t('Not found TEP74 address for this token'))); } - return [errors, keypair, transferValue]; + return errors; } export function additionalValidateTransferForRecipient ( @@ -125,16 +119,15 @@ export function additionalValidateTransferForRecipient ( } // xcm transfer -export function validateXcmTransferRequest (destTokenInfo: _ChainAsset | undefined, sender: _Address, sendingValue: string): [TransactionError[], KeyringPair | undefined, BigN | undefined] { +export function validateXcmTransferRequest (destTokenInfo: _ChainAsset | undefined, sender: _Address, sendingValue: string): [TransactionError[], KeyringPair | undefined] { const errors = [] as TransactionError[]; const keypair = keyring.getPair(sender); - const transferValue = new BigN(sendingValue); if (!destTokenInfo) { errors.push(new TransactionError(TransferTxErrorType.INVALID_TOKEN, t('Not found token from registry'))); } - return [errors, keypair, transferValue]; + return [errors, keypair]; } export function additionalValidateXcmTransfer (originTokenInfo: _ChainAsset, destinationTokenInfo: _ChainAsset, sendingAmount: string, senderTransferable: string, receiverNativeBalance: string, destChainInfo: _ChainInfo, isSnowBridge = false): [TransactionWarning | undefined, TransactionError | undefined] { @@ -370,7 +363,7 @@ export function checkSupportForTransaction (validationResponse: SWTransactionRes } } -export async function estimateFeeForTransaction (validationResponse: SWTransactionResponse, transaction: OptionalSWTransaction, chainInfo: _ChainInfo, evmApi: _EvmApi): Promise { +export async function estimateFeeForTransaction (validationResponse: SWTransactionResponse, transaction: OptionalSWTransaction, chainInfo: _ChainInfo, evmApi: _EvmApi, substrateApi: _SubstrateApi, feeInfo: EvmFeeInfo, nativeTokenInfo: _ChainAsset, tokenPayFeeInfo: _ChainAsset | undefined, isTransferLocalTokenAndPayThatTokenAsFee: boolean | undefined): Promise { const estimateFee: FeeData = { symbol: '', decimals: 0, @@ -391,23 +384,23 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti } else { const gasLimit = transaction.gas || await evmApi.api.eth.estimateGas(transaction); - const priority = await calculateGasFeeParams(evmApi, chainInfo.slug); + const feeCombine = combineEthFee(feeInfo, validationResponse.feeOption, validationResponse.feeCustom as EvmEIP1559FeeOption); if (transaction.maxFeePerGas) { estimateFee.value = new BigN(transaction.maxFeePerGas.toString()).multipliedBy(gasLimit).toFixed(0); } else if (transaction.gasPrice) { - estimateFee.value = new BigN((transaction.gasPrice || 0).toString()).multipliedBy(gasLimit).toFixed(0); + estimateFee.value = new BigN(transaction.gasPrice.toString()).multipliedBy(gasLimit).toFixed(0); } else { - if (priority.baseGasFee) { - const maxFee = priority.maxFeePerGas; // TODO: Need review + if (feeCombine.maxFeePerGas) { + const maxFee = new BigN(feeCombine.maxFeePerGas); // TODO: Need review estimateFee.value = maxFee.multipliedBy(gasLimit).toFixed(0); - } else { - estimateFee.value = new BigN(priority.gasPrice).multipliedBy(gasLimit).toFixed(0); + } else if (feeCombine.gasPrice) { + estimateFee.value = new BigN((feeCombine.gasPrice || 0)).multipliedBy(gasLimit).toFixed(0); } } - estimateFee.tooHigh = priority.busyNetwork; + estimateFee.tooHigh = feeInfo.busyNetwork; } } catch (e) { const error = e as Error; @@ -418,6 +411,14 @@ export async function estimateFeeForTransaction (validationResponse: SWTransacti } } + if (tokenPayFeeInfo) { + const estimatedFeeAmount = isTransferLocalTokenAndPayThatTokenAsFee ? (BigInt(estimateFee.value) * BigInt(FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE) / BigInt(100)).toString() : estimateFee.value; + + estimateFee.decimals = tokenPayFeeInfo.decimals || 0; + estimateFee.symbol = tokenPayFeeInfo.symbol; + estimateFee.value = await calculateToAmountByReservePool(substrateApi.api, nativeTokenInfo, tokenPayFeeInfo, estimatedFeeAmount); + } + return estimateFee; } @@ -449,9 +450,9 @@ export function checkBalanceWithTransactionFee (validationResponse: SWTransactio return; } - const { edAsWarning, extrinsicType, isTransferAll, skipFeeValidation } = transactionInput; + const { edAsWarning, extrinsicType, isTransferAll, nonNativeTokenPayFeeSlug, skipFeeValidation } = transactionInput; - if (skipFeeValidation) { + if (skipFeeValidation || nonNativeTokenPayFeeSlug) { return; } diff --git a/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts b/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts index 7d8be82fe15..8140aec67de 100644 --- a/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts +++ b/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts @@ -5,6 +5,7 @@ import { _Address } from '@subwallet/extension-base/background/KoniTypes'; import { _ERC20_ABI } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { TransactionConfig } from 'web3-core'; import { Contract } from 'web3-eth-contract'; @@ -38,6 +39,7 @@ export async function getERC20SpendingApprovalTx (spender: _Address, owner: _Add // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access const gasLimit = await approveCall.estimateGas({ from: owner }) as number; const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + const feeCombine = combineEthFee(priority); return { from: owner, @@ -45,7 +47,6 @@ export async function getERC20SpendingApprovalTx (spender: _Address, owner: _Add data: approveEncodedCall, gas: gasLimit, gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; } diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 51e58166002..b47f8aa0c29 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -3,15 +3,14 @@ import { Common } from '@ethereumjs/common'; import { LegacyTransaction } from '@ethereumjs/tx'; -import { COMMON_CHAIN_SLUGS } from '@subwallet/chain-list'; import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from '@subwallet/chain-list/types'; 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, SufficientMetadata, ThemeNames, TokenPriorityDetails, 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, 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, TokenPriorityDetails, 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 { ALL_ACCOUNT_KEY, LATEST_SESSION } from '@subwallet/extension-base/constants'; 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'; @@ -30,33 +29,38 @@ import KoniState from '@subwallet/extension-base/koni/background/handlers/State' import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers/process'; import { isBounceableAddress } from '@subwallet/extension-base/services/balance-service/helpers/subscribe/ton/utils'; import { getERC20TransactionObject, getERC721Transaction, getEVMTransactionObject, getPSP34TransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; -import { createTransferExtrinsic, getTransferMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/token'; +import { createTransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { createTonTransaction } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; -import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic, getXcmMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; +import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic } 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 { _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 { _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, _getContractAddressOfToken, _getEvmChainId, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; +import { calculateToAmountByReservePool, FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE } from '@subwallet/extension-base/services/fee-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'; import { AuthUrls } from '@subwallet/extension-base/services/request-service/types'; import { DEFAULT_AUTO_LOCK_TIME } from '@subwallet/extension-base/services/setting-service/constants'; +import { checkLiquidityForPath, estimateTokensForPath, getReserveForPath } from '@subwallet/extension-base/services/swap-service/handler/asset-hub/utils'; import { SWTransaction, SWTransactionResponse, SWTransactionResult, TransactionEmitter, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectNamespace } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { SWStorage } from '@subwallet/extension-base/storage'; import { AccountsStore } from '@subwallet/extension-base/stores'; -import { AccountJson, AccountProxyMap, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BuyServiceInfo, BuyTokenInfo, EarningRewardJson, NominationPoolInfo, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StakingTxErrorType, StorageDataInterface, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; +import { AccountJson, AccountProxyMap, AccountsWithCurrentAddress, BalanceJson, BasicTxErrorType, BasicTxWarningCode, BuyServiceInfo, BuyTokenInfo, EarningRewardJson, EvmFeeInfo, FeeChainType, FeeInfo, NominationPoolInfo, OptimalYieldPathParams, RequestAccountBatchExportV2, RequestAccountCreateSuriV2, RequestAccountNameValidate, RequestBatchJsonGetAccountInfo, RequestBatchRestoreV2, RequestBounceableValidate, RequestChangeTonWalletContractVersion, RequestCheckPublicAndSecretKey, RequestCrossChainTransfer, RequestDeriveCreateMultiple, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestEarlyValidateYield, RequestExportAccountProxyMnemonic, RequestGetAllTonWalletContractVersion, RequestGetAmountForPair, RequestGetDeriveAccounts, RequestGetDeriveSuggestion, RequestGetTokensCanPayFee, RequestGetYieldPoolTargets, RequestInputAccountSubscribe, RequestJsonGetAccountInfo, RequestJsonRestoreV2, RequestMetadataHash, RequestMnemonicCreateV2, RequestMnemonicValidateV2, RequestPrivateKeyValidateV2, RequestShortenMetadata, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseAccountBatchExportV2, ResponseAccountCreateSuriV2, ResponseAccountNameValidate, ResponseBatchJsonGetAccountInfo, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseExportAccountProxyMnemonic, ResponseGetAllTonWalletContractVersion, ResponseGetDeriveAccounts, ResponseGetDeriveSuggestion, ResponseGetYieldPoolTargets, ResponseInputAccountSubscribe, ResponseJsonGetAccountInfo, ResponseMetadataHash, ResponseMnemonicCreateV2, ResponseMnemonicValidateV2, ResponsePrivateKeyValidateV2, ResponseShortenMetadata, StakingTxErrorType, StorageDataInterface, TokenSpendingApprovalParams, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; import { RequestAccountProxyEdit, RequestAccountProxyForget } from '@subwallet/extension-base/types/account/action/edit'; +import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { RequestClaimBridge } from '@subwallet/extension-base/types/bridge'; import { GetNotificationParams, RequestIsClaimedPolygonBridge, RequestSwitchStatusParams } from '@subwallet/extension-base/types/notification'; import { CommonOptimalPath } from '@subwallet/extension-base/types/service-base'; import { SwapPair, SwapQuoteResponse, SwapRequest, SwapRequestResult, SwapSubmitParams, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; -import { _analyzeAddress, BN_ZERO, combineAllAccountProxy, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, toBNString, Transaction as QrTransaction, transformAccounts, transformAddresses, uniqueStringArray } from '@subwallet/extension-base/utils'; +import { _analyzeAddress, CalculateMaxTransferable, calculateMaxTransferable, combineAllAccountProxy, createTransactionFromRLP, detectTransferTxType, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, Transaction as QrTransaction, transformAccounts, transformAddresses, uniqueStringArray } from '@subwallet/extension-base/utils'; import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { MetadataDef } from '@subwallet/extension-inject/types'; import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; import { EthereumKeypairTypes, SubstrateKeypairTypes, TonKeypairTypes } from '@subwallet/keyring/types'; @@ -74,7 +78,7 @@ import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; import { TypeRegistry } from '@polkadot/types'; import { AnyJson, Registry, SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; -import { assert, hexStripPrefix, hexToU8a, isAscii, isHex, u8aToHex } from '@polkadot/util'; +import { assert, hexStripPrefix, hexToU8a, isAscii, isHex, noop, u8aToHex } from '@polkadot/util'; import { decodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; import { getSuitableRegistry, RegistrySource, setupApiRegistry, setupDappRegistry, setupDatabaseRegistry } from '../utils'; @@ -1307,15 +1311,15 @@ export default class KoniExtension { }); } - private async makeTransfer (inputData: RequestTransfer): Promise { - const { from, networkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; + private async makeTransfer (inputData: RequestSubmitTransfer): Promise { + const { chain, feeCustom, feeOption, from, isTransferLocalTokenAndPayThatTokenAsFee, nonNativeTokenPayFeeSlug, to, tokenSlug, transferAll, transferBounceable, value } = inputData; const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(tokenSlug); - const [errors, ,] = validateTransferRequest(transferTokenInfo, from, to, value, transferAll); + const errors = validateTransferRequest(transferTokenInfo, from, to, value, transferAll); const warnings: TransactionWarning[] = []; - const chainInfo = this.#koniState.getChainInfo(networkKey); + const chainInfo = this.#koniState.getChainInfo(chain); - const nativeTokenInfo = this.#koniState.getNativeTokenInfo(networkKey); + const nativeTokenInfo = this.#koniState.getNativeTokenInfo(chain); const nativeTokenSlug: string = nativeTokenInfo.slug; const isTransferNativeToken = nativeTokenSlug === tokenSlug; const extrinsicType = isTransferNativeToken ? ExtrinsicType.TRANSFER_BALANCE : ExtrinsicType.TRANSFER_TOKEN; @@ -1325,13 +1329,14 @@ export default class KoniExtension { let transaction: ValidateTransactionResponseInput['transaction']; - const transferTokenAvailable = await this.getAddressTransferableBalance({ address: from, networkKey, token: tokenSlug, extrinsicType }); + const transferTokenAvailable = await this.getAddressTransferableBalance({ address: from, networkKey: chain, token: tokenSlug, extrinsicType }); try { if (isEthereumAddress(from) && isEthereumAddress(to) && _isTokenTransferredByEvm(transferTokenInfo)) { chainType = ChainType.EVM; const txVal: string = transferAll ? transferTokenAvailable.value : (value || '0'); - const evmApi = this.#koniState.getEvmApi(networkKey); + const evmApi = this.#koniState.getEvmApi(chain); + const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'evm'); // todo: refactor: merge getERC20TransactionObject & getEVMTransactionObject // Estimate with EVM API @@ -1339,37 +1344,58 @@ export default class KoniExtension { [ transaction, transferAmount.value - ] = await getERC20TransactionObject(_getContractAddressOfToken(transferTokenInfo), chainInfo, from, to, txVal, !!transferAll, evmApi); + ] = await getERC20TransactionObject({ + assetAddress: _getContractAddressOfToken(transferTokenInfo), + chain, + evmApi, + feeCustom, + feeInfo, + feeOption, + from, + to, + transferAll, + value: txVal + }); } else { [ transaction, transferAmount.value - ] = await getEVMTransactionObject(chainInfo, from, to, txVal, !!transferAll, evmApi); + ] = await getEVMTransactionObject({ + chain, + evmApi, + feeCustom, + feeInfo, + feeOption, + from, + to, + transferAll, + value: txVal + }); } } else if (_isMantaZkAsset(transferTokenInfo)) { transaction = undefined; transferAmount.value = '0'; } else if (isTonAddress(from) && isTonAddress(to) && _isTokenTransferredByTon(transferTokenInfo)) { chainType = ChainType.TON; - const tonApi = this.#koniState.getTonApi(networkKey); + const tonApi = this.#koniState.getTonApi(chain); [transaction, transferAmount.value] = await createTonTransaction({ tokenInfo: transferTokenInfo, from, to, - networkKey, + networkKey: chain, value: value || '0', transferAll: !!transferAll, // currently not used tonApi }); } else { - const substrateApi = this.#koniState.getSubstrateApi(networkKey); + const substrateApi = this.#koniState.getSubstrateApi(chain); [transaction, transferAmount.value] = await createTransferExtrinsic({ transferAll: !!transferAll, value: value || '0', from: from, - networkKey, + networkKey: chain, tokenInfo: transferTokenInfo, to: to, substrateApi @@ -1395,23 +1421,35 @@ export default class KoniExtension { return undefined; } + // Check enough free local to pay fee local + if (nonNativeTokenPayFeeSlug) { + const nonNativeFee = BigInt(inputTransaction.estimateFee?.value || '0'); // todo: estimateFee should be must-have, need to refactor interface + const proxyId = this.#koniState.keyringService.context.currentAccount.proxyId; + const nonNativeTokenPayFeeInfo = await this.#koniState.balanceService.getTokensHasBalance(proxyId, chain, nonNativeTokenPayFeeSlug); + const nonNativeTokenPayFeeBalance = BigInt(nonNativeTokenPayFeeInfo[nonNativeTokenPayFeeSlug].free); + + if (nonNativeFee > nonNativeTokenPayFeeBalance) { + inputTransaction.errors.push(new TransactionError(BasicTxErrorType.NOT_ENOUGH_BALANCE)); + } + } + // Check ed for sender if (!isTransferNativeToken) { const [_senderSendingTokenTransferable, _receiverNativeTotal] = await Promise.all([ - this.getAddressTransferableBalance({ address: from, networkKey, token: tokenSlug, extrinsicType }), - this.getAddressTotalBalance({ address: to, networkKey, token: nativeTokenSlug, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }) + this.getAddressTransferableBalance({ address: from, networkKey: chain, token: tokenSlug, extrinsicType }), + this.getAddressTotalBalance({ address: to, networkKey: chain, token: nativeTokenSlug, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }) ]); senderSendingTokenTransferable = BigInt(_senderSendingTokenTransferable.value); receiverSystemAccountInfo = _receiverNativeTotal.metadata as FrameSystemAccountInfo; } - const { value: _receiverSendingTokenKeepAliveBalance } = await this.getAddressTotalBalance({ address: to, networkKey, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts + const { value: _receiverSendingTokenKeepAliveBalance } = await this.getAddressTotalBalance({ address: to, networkKey: chain, token: tokenSlug, extrinsicType }); // todo: shouldn't be just transferable, locked also counts const receiverSendingTokenKeepAliveBalance = BigInt(_receiverSendingTokenKeepAliveBalance); const amount = BigInt(transferAmount.value); - const substrateApi = this.#koniState.getSubstrateApi(networkKey); + const substrateApi = this.#koniState.getSubstrateApi(chain); const isSendingTokenSufficient = await this.isSufficientToken(transferTokenInfo, substrateApi); const [warnings, errors] = additionalValidateTransferForRecipient(transferTokenInfo, nativeTokenInfo, extrinsicType, receiverSendingTokenKeepAliveBalance, amount, senderSendingTokenTransferable, receiverSystemAccountInfo, isSendingTokenSufficient); @@ -1434,7 +1472,10 @@ export default class KoniExtension { errors, warnings, address: from, - chain: networkKey, + chain, + feeCustom, + feeOption, + nonNativeTokenPayFeeSlug, chainType, transferNativeAmount, transaction, @@ -1442,13 +1483,14 @@ export default class KoniExtension { extrinsicType, ignoreWarnings, isTransferAll: isTransferNativeToken ? transferAll : false, + isTransferLocalTokenAndPayThatTokenAsFee, edAsWarning: isTransferNativeToken, additionalValidator: additionalValidator }); } private async makeCrossChainTransfer (inputData: RequestCrossChainTransfer): Promise { - const { destinationNetworkKey, from, originNetworkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; + const { destinationNetworkKey, feeCustom, feeOption, from, isTransferLocalTokenAndPayThatTokenAsFee, nonNativeTokenPayFeeSlug, originNetworkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; const originTokenInfo = this.#koniState.getAssetBySlug(tokenSlug); const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destinationNetworkKey, tokenSlug); @@ -1472,31 +1514,44 @@ export default class KoniExtension { if (fromKeyPair && destinationTokenInfo) { const evmApi = this.#koniState.getEvmApi(originNetworkKey); const substrateApi = this.#koniState.getSubstrateApi(originNetworkKey); - const params: CreateXcmExtrinsicProps = { - destinationTokenInfo, - originTokenInfo, - sendingValue: value, - sender: from, - recipient: to, - chainInfoMap, - substrateApi, - evmApi - }; let funcCreateExtrinsic: FunctionCreateXcmExtrinsic; + let type: FeeChainType; if (isPosBridgeTransfer || isPolygonBridgeTransfer) { funcCreateExtrinsic = createPolygonBridgeExtrinsic; + type = 'evm'; } else if (isSnowBridgeEvmTransfer) { funcCreateExtrinsic = createSnowBridgeExtrinsic; + type = 'evm'; } else if (isAvailBridgeFromEvm) { funcCreateExtrinsic = createAvailBridgeTxFromEth; + type = 'evm'; } else if (isAvailBridgeFromAvail) { funcCreateExtrinsic = createAvailBridgeExtrinsicFromAvail; + type = 'substrate'; } else { funcCreateExtrinsic = createXcmExtrinsic; + type = 'substrate'; } + const feeInfo: FeeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), originNetworkKey, type); + + const params: CreateXcmExtrinsicProps = { + destinationTokenInfo, + originTokenInfo, + sendingValue: value, + sender: from, + recipient: to, + destinationChain: chainInfoMap[destinationTokenInfo.originChain], + originChain: chainInfoMap[originTokenInfo.originChain], + substrateApi, + evmApi, + feeCustom, + feeOption, + feeInfo + }; + extrinsic = await funcCreateExtrinsic(params); additionalValidator = async (inputTransaction: SWTransactionResponse): Promise => { @@ -1554,13 +1609,75 @@ export default class KoniExtension { chainType: !isSnowBridgeEvmTransfer && !isAvailBridgeFromEvm && !isPolygonBridgeTransfer && !isPosBridgeTransfer ? ChainType.SUBSTRATE : ChainType.EVM, transferNativeAmount: _isNativeToken(originTokenInfo) ? value : '0', ignoreWarnings, + nonNativeTokenPayFeeSlug, isTransferAll: transferAll, + isTransferLocalTokenAndPayThatTokenAsFee, errors, additionalValidator: additionalValidator, eventsHandler: eventsHandler }); } + private async getTokensCanPayFee (request: RequestGetTokensCanPayFee): Promise { + const { chain, feeAmount, proxyId } = request; + + const chainService = this.#koniState.chainService; + const substrateApi = this.#koniState.getSubstrateApi(chain); + + const tokensHasBalanceInfoMap = await this.#koniState.balanceService.getTokensHasBalance(proxyId, chain); + const tokensHasBalanceSlug = Object.keys(tokensHasBalanceInfoMap); + const tokensHasBalanceInfo = tokensHasBalanceSlug.map((tokenSlug) => chainService.getAssetBySlug(tokenSlug)).filter((token) => token.assetType !== _AssetType.NATIVE && token.metadata && token.metadata.multilocation); + + const nativeTokenInfo = chainService.getNativeTokenInfo(chain); + + if (!nativeTokenInfo.metadata || !nativeTokenInfo.metadata.multilocation) { + return []; + } + + const nativeMultiLocation = nativeTokenInfo.metadata.multilocation; + + const tokensCanPayFeeSlug: string[] = [nativeTokenInfo.slug]; + + await Promise.all(tokensHasBalanceInfo.map(async (tokenInfo) => { + const tokenSlug = tokenInfo.slug; + // @ts-ignore + const tokenMultiLocation = tokenInfo.metadata.multilocation; + + const _poolInfo = await substrateApi.api.query.assetConversion.pools([nativeMultiLocation, tokenMultiLocation]); + const poolInfo = _poolInfo.toPrimitive() as { lpToken: string} || null; + + if (poolInfo && poolInfo.lpToken !== undefined) { + if (feeAmount === undefined) { + tokensCanPayFeeSlug.push(tokenSlug); + } else { + const reserves = await getReserveForPath(substrateApi.api, [nativeTokenInfo, tokenInfo]); + const amounts = estimateTokensForPath(feeAmount, reserves); + const liquidityError = checkLiquidityForPath(amounts, reserves); + + if (!liquidityError) { + tokensCanPayFeeSlug.push(tokenSlug); + } + } + } + })); + + return tokensCanPayFeeSlug.map((slug) => tokensHasBalanceInfoMap[slug]); + } + + private async getAmountForPair (request: RequestGetAmountForPair) { + const { nativeTokenFeeAmount, nativeTokenSlug, toTokenSlug } = request; + + if (nativeTokenSlug === toTokenSlug) { + return nativeTokenFeeAmount; + } + + const nativeTokenInfo = this.#koniState.chainService.getAssetBySlug(nativeTokenSlug); + const toTokenInfo = this.#koniState.chainService.getAssetBySlug(toTokenSlug); + const substrateApi = this.#koniState.chainService.getSubstrateApi(nativeTokenInfo.originChain); + + return await calculateToAmountByReservePool(substrateApi.api, nativeTokenInfo, toTokenInfo, nativeTokenFeeAmount); + } + private async evmNftSubmitTransaction (inputData: NftTransactionRequest): Promise { const { networkKey, params, recipientAddress, senderAddress } = inputData; const contractAddress = params.contractAddress as string; @@ -1578,7 +1695,8 @@ export default class KoniExtension { }); } - const transaction = await getERC721Transaction(this.#koniState.getEvmApi(networkKey), networkKey, contractAddress, senderAddress, recipientAddress, tokenId); + const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), networkKey, 'evm'); + const transaction = await getERC721Transaction(this.#koniState.getEvmApi(networkKey), networkKey, contractAddress, senderAddress, recipientAddress, tokenId, feeInfo); // this.addContact(recipientAddress); @@ -1750,74 +1868,111 @@ export default class KoniExtension { 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); + private async subscribeMaxTransferable (request: RequestSubscribeTransfer, id: string, port: chrome.runtime.Port): Promise { + const { address, chain, destChain: _destChain, feeCustom, feeOption, nonNativeTokenPayFeeSlug, token } = request; + const cb = createSubscription<'pri(transfer.subscribe)'>(id, port); - if (!_isNativeToken(tokenInfo)) { - return await this.getAddressTransferableBalance({ - extrinsicType: ExtrinsicType.TRANSFER_TOKEN, - address, - networkKey, - token - }); - } else { - let maxTransferable: BigN; + const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(token); + const isTransferLocalTokenAndPayThatTokenAsFee = !_isNativeToken(transferTokenInfo) && nonNativeTokenPayFeeSlug && nonNativeTokenPayFeeSlug === token; + const isTransferNativeTokenAndPayLocalTokenAsFee = _isNativeToken(transferTokenInfo) && nonNativeTokenPayFeeSlug; + const srcToken = token ? this.#koniState.chainService.getAssetBySlug(token) : this.#koniState.chainService.getNativeTokenInfo(chain); + const destToken = _destChain !== chain ? this.#koniState.getXcmEqualAssetByChain(_destChain, srcToken.slug) as _ChainAsset : srcToken; + const srcChain = this.#koniState.chainService.getChainInfoByKey(chain); + const destChain = this.#koniState.chainService.getChainInfoByKey(_destChain); - if (isXcmTransfer) { - maxTransferable = await this.getXcmMaxTransferable(tokenInfo, destChain, address); - } else { - // regular transfer with native token - maxTransferable = await this.getNativeTokenMaxTransferable(tokenInfo, networkKey, address); - } + const freeBalanceSubject = new Subject(); + const feeSubject = new Subject(); + const feeChainType: FeeChainType = detectTransferTxType(srcToken, srcChain, destChain); - return { - value: maxTransferable.gt(BN_ZERO) ? (maxTransferable.toFixed(0) || '0') : '0', - decimals: tokenInfo.decimals, - symbol: tokenInfo.symbol - } as AmountData; + if (!destToken) { + throw new Error('Destination token not found'); } - } - private async getXcmMaxTransferable (originTokenInfo: _ChainAsset, destChain: string, address: string): Promise { - const substrateApi = this.#koniState.chainService.getSubstrateApi(originTokenInfo.originChain); - const chainInfoMap = this.#koniState.chainService.getChainInfoMap(); - const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destChain, originTokenInfo.slug); + const recalculateMaxTransferableSpecialCase = async (transferInfo: ResponseSubscribeTransfer): Promise => { + if (isTransferLocalTokenAndPayThatTokenAsFee) { + const nativeTokenSlug = this.#koniState.chainService.getNativeTokenInfo(chain).slug; + const estimatedFeeNative = (BigInt(transferInfo.feeOptions.estimatedFee) * BigInt(FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE) / BigInt(100)).toString(); + const estimatedFeeLocal = await this.getAmountForPair({ + nativeTokenSlug: nativeTokenSlug, + toTokenSlug: token, + nativeTokenFeeAmount: estimatedFeeNative + }); - // todo: improve this case. Currently set 1 AVAIL for covering fee as default. - const isSpecialBridgeFromAvail = originTokenInfo.slug === 'avail_mainnet-NATIVE-AVAIL' && destChain === COMMON_CHAIN_SLUGS.ETHEREUM; - const specialBridgeFromAvailFee = new BigN(toBNString(1, _getAssetDecimals(originTokenInfo))).minus(new BigN(_getTokenMinAmount(originTokenInfo))); + transferInfo.feeOptions.estimatedFee = estimatedFeeNative; + transferInfo.maxTransferable = (BigInt(transferInfo.maxTransferable) - BigInt(estimatedFeeLocal)).toString(); + } - if (destinationTokenInfo) { - const [bnMockExecutionFee, { value }] = await Promise.all([ - getXcmMockTxFee(substrateApi, chainInfoMap, originTokenInfo, destinationTokenInfo), - this.getAddressTransferableBalance({ extrinsicType: ExtrinsicType.TRANSFER_XCM, address, networkKey: originTokenInfo.originChain, token: originTokenInfo.slug }) - ]); + if (isTransferNativeTokenAndPayLocalTokenAsFee) { + transferInfo.maxTransferable = (BigInt(transferInfo.maxTransferable) + BigInt(transferInfo.feeOptions.estimatedFee)).toString(); + } - const bnMaxTransferable = new BigN(value); - const txFee = isSpecialBridgeFromAvail ? specialBridgeFromAvailFee : bnMockExecutionFee.multipliedBy(XCM_FEE_RATIO); + return transferInfo; + }; - return bnMaxTransferable.minus(txFee); - } + const _request: CalculateMaxTransferable = { + address: address, + destChain, + destToken, + evmApi: this.#koniState.chainService.getEvmApi(chain), + feeCustom, + feeOption, + srcChain, + srcToken, + substrateApi: this.#koniState.chainService.getSubstrateApi(chain), + tonApi: this.#koniState.chainService.getTonApi(chain), + recalculateMaxTransferableSpecialCase + }; - return new BigN(0); - } + const subscription = combineLatest({ + freeBalance: freeBalanceSubject, + fee: feeSubject + }) + .subscribe({ + next: ({ fee, freeBalance }) => { + calculateMaxTransferable(id, _request, freeBalance, fee) + .then(cb) + .catch(console.error); + } + }); + + const [unsubBalance, freeBalance] = await (async () => { + try { + return await this.#koniState.balanceService.subscribeBalance(address, chain, token, 'transferable', undefined, (data) => { + freeBalanceSubject.next(data); // Must be called after subscription + }); + } catch (e) { + const fallBackValue: AmountData = { + value: '0', + decimals: srcToken.decimals || 0, + symbol: srcToken.symbol + }; - private async getNativeTokenMaxTransferable (tokenInfo: _ChainAsset, networkKey: string, address: string): Promise { - const chainInfo = this.#koniState.chainService.getChainInfoByKey(networkKey); - const api = _isChainEvmCompatible(chainInfo) && _isTokenTransferredByEvm(tokenInfo) - ? this.#koniState.chainService.getEvmApi(networkKey) - : _isChainTonCompatible(chainInfo) && _isTokenTransferredByTon(tokenInfo) - ? this.#koniState.chainService.getTonApi(networkKey) - : this.#koniState.chainService.getSubstrateApi(networkKey); + freeBalanceSubject.next(fallBackValue); - const [mockTxFee, { value }] = await Promise.all([ - getTransferMockTxFee(address, chainInfo, tokenInfo, api), - this.getAddressTransferableBalance({ extrinsicType: ExtrinsicType.TRANSFER_BALANCE, address, networkKey, token: tokenInfo.slug }) - ]); + return [noop, fallBackValue]; + } + })(); + + const fee = await this.#koniState.feeService.subscribeChainFee(id, chain, feeChainType, (data) => { + feeSubject.next(data); // Must be called after subscription + }); - const bnMaxTransferable = new BigN(value); + const unsub = () => { + subscription.unsubscribe(); + unsubBalance(); + this.#koniState.feeService.unsubscribeChainFee(id, chain, feeChainType); + }; - return bnMaxTransferable.minus(mockTxFee); + this.createUnsubscriptionHandle( + id, + unsub + ); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return calculateMaxTransferable(id, _request, freeBalance, fee); } private async subscribeAddressTransferableBalance ({ address, extrinsicType, networkKey, token }: RequestFreeBalance, id: string, port: chrome.runtime.Port): Promise { @@ -3827,8 +3982,9 @@ export default class KoniExtension { chainType = ChainType.SUBSTRATE; } else { const evmApi = this.#koniState.getEvmApi(chain); + const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'evm') as EvmFeeInfo; - transaction = await getClaimTxOnEthereum(chain, notification, evmApi); + transaction = await getClaimTxOnEthereum(chain, notification, evmApi, feeInfo); chainType = ChainType.EVM; } @@ -3858,11 +4014,12 @@ export default class KoniExtension { const evmApi = this.#koniState.getEvmApi(chain); const metadata = notification.metadata as ClaimPolygonBridgeNotificationMetadata; + const feeInfo = await this.#koniState.feeService.subscribeChainFee(getId(), chain, 'evm') as EvmFeeInfo; if (metadata.bridgeType === 'POS') { - transaction = await getClaimPosBridge(chain, notification, evmApi); + transaction = await getClaimPosBridge(chain, notification, evmApi, feeInfo); } else { - transaction = await getClaimPolygonBridge(chain, notification, evmApi); + transaction = await getClaimPolygonBridge(chain, notification, evmApi, feeInfo); } const chainType: ChainType = ChainType.EVM; @@ -4241,8 +4398,9 @@ export default class KoniExtension { case 'pri(assetSetting.update)': return await this.updateAssetSetting(request as AssetSettingUpdateReq); - case 'pri(transfer.getMaxTransferable)': - return this.getMaxTransferable(request as RequestMaxTransferable); + case 'pri(transfer.subscribe)': + return this.subscribeMaxTransferable(request as RequestSubscribeTransfer, id, port); + case 'pri(freeBalance.get)': return this.getAddressTransferableBalance(request as RequestFreeBalance); case 'pri(freeBalance.subscribe)': @@ -4260,13 +4418,17 @@ export default class KoniExtension { /// Transfer case 'pri(accounts.transfer)': - return await this.makeTransfer(request as RequestTransfer); + return await this.makeTransfer(request as RequestSubmitTransfer); case 'pri(accounts.crossChainTransfer)': return await this.makeCrossChainTransfer(request as RequestCrossChainTransfer); case 'pri(accounts.getOptimalTransferProcess)': return await this.getOptimalTransferProcess(request as RequestOptimalTransferProcess); case 'pri(accounts.approveSpending)': return await this.approveSpending(request as TokenSpendingApprovalParams); + case 'pri(customFee.getTokensCanPayFee)': + return this.getTokensCanPayFee(request as RequestGetTokensCanPayFee); + case 'pri(customFee.getAmountForPair)': + return this.getAmountForPair(request as RequestGetAmountForPair); /// Sign QR case 'pri(qr.transaction.parse.substrate)': @@ -4515,10 +4677,8 @@ export default class KoniExtension { /* Avail Bridge */ /* Polygon Bridge */ - case 'pri(polygonBridge.submitClaimPolygonBridge)': return this.submitClaimPolygonBridge(request as RequestClaimBridge); - /* Polygon Bridge */ /* Ledger */ diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 98a78580cd4..e9793dd1265 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -1155,15 +1155,7 @@ export default class KoniState { })(); promiseList.push(Promise.race([promise, timeoutPromise]).then((result) => { - return [slug, result - ? { - ...result, - gasPrice: result.gasPrice?.toString(), - maxFeePerGas: result.maxFeePerGas?.toString(), - maxPriorityFeePerGas: result.maxPriorityFeePerGas?.toString(), - baseGasFee: result.baseGasFee?.toString() - } as EvmFeeInfo - : null]; + return [slug, result || null]; })); }); diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 9db8df64e1b..81cc5645949 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -10,6 +10,7 @@ import { getDefaultTransferProcess, getSnowbridgeTransferProcessFromEvm, Request import { ServiceStatus, StoppableServiceInterface } from '@subwallet/extension-base/services/base/types'; import { _getChainNativeTokenSlug, _isPureEvmChain } from '@subwallet/extension-base/services/chain-service/utils'; import { EventItem, EventType } from '@subwallet/extension-base/services/event-service/types'; +import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; import DetectAccountBalanceStore from '@subwallet/extension-base/stores/DetectAccountBalance'; import { BalanceItem, BalanceJson } from '@subwallet/extension-base/types'; import { CommonOptimalPath } from '@subwallet/extension-base/types/service-base'; @@ -321,6 +322,26 @@ export class BalanceService implements StoppableServiceInterface { return await this.state.dbService.stores.balance.getBalanceMapByAddresses(address); } + public async getTokensHasBalance (proxyId: string, chain: string, tokenSlug?: string): Promise> { + const balanceItems = await this.state.dbService.stores.balance.getBalanceHasAmount(proxyId, chain); + const tokenHasBalanceInfoMap: Record = {}; + + balanceItems.forEach((balanceItem) => { + tokenHasBalanceInfoMap[balanceItem.tokenSlug] = { + slug: balanceItem.tokenSlug, + free: balanceItem.free + } as TokenHasBalanceInfo; + }); + + if (tokenSlug) { + return { + [tokenSlug]: tokenHasBalanceInfoMap[tokenSlug] + }; + } + + return tokenHasBalanceInfoMap; + } + public async handleResetBalance (forceRefresh?: boolean) { if (forceRefresh) { this.balanceMap.setData({}); diff --git a/packages/extension-base/src/services/balance-service/transfer/smart-contract.ts b/packages/extension-base/src/services/balance-service/transfer/smart-contract.ts index c53d3892922..dd7e90c8799 100644 --- a/packages/extension-base/src/services/balance-service/transfer/smart-contract.ts +++ b/packages/extension-base/src/services/balance-service/transfer/smart-contract.ts @@ -1,57 +1,66 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import { _ChainInfo } from '@subwallet/chain-list/types'; import { getERC20Contract } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; import { _ERC721_ABI } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { getPSP34ContractPromise } from '@subwallet/extension-base/koni/api/contract-handler/wasm'; import { getWasmContractGasLimit } from '@subwallet/extension-base/koni/api/contract-handler/wasm/utils'; import { EVM_REFORMAT_DECIMALS } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; -import { EvmFeeInfo } from '@subwallet/extension-base/types'; +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeInfo, TransactionFee } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; import { t } from 'i18next'; import { TransactionConfig } from 'web3-core'; -export async function getEVMTransactionObject ( - chainInfo: _ChainInfo, - from: string, - to: string, - value: string, - transferAll: boolean, - web3Api: _EvmApi -): Promise<[TransactionConfig, string]> { - const networkKey = chainInfo.slug; +interface TransferEvmProps extends TransactionFee { + chain: string; + from: string; + feeInfo: FeeInfo; + to: string; + transferAll: boolean; + value: string; + evmApi: _EvmApi; +} + +export async function getEVMTransactionObject ({ chain, + evmApi, + feeCustom: _feeCustom, + feeInfo: _feeInfo, + feeOption, + from, + to, + transferAll, + value }: TransferEvmProps): Promise<[TransactionConfig, string]> { + const feeCustom = _feeCustom as EvmEIP1559FeeOption; + const feeInfo = _feeInfo as EvmFeeInfo; - const priority = await calculateGasFeeParams(web3Api, networkKey); + const feeCombine = combineEthFee(feeInfo, feeOption, feeCustom); const transactionObject = { to: to, value: value, from: from, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; - const gasLimit = await web3Api.api.eth.estimateGas(transactionObject); + const gasLimit = await evmApi.api.eth.estimateGas(transactionObject); transactionObject.gas = gasLimit; let estimateFee: BigN; - if (priority.baseGasFee) { - const maxFee = priority.maxFeePerGas; + if (feeCombine.maxFeePerGas) { + const maxFee = new BigN(feeCombine.maxFeePerGas); estimateFee = maxFee.multipliedBy(gasLimit); } else { - estimateFee = new BigN(priority.gasPrice).multipliedBy(gasLimit); + estimateFee = new BigN(feeCombine.gasPrice || '0').multipliedBy(gasLimit); } transactionObject.value = transferAll ? new BigN(value).minus(estimateFee).toString() : value; - if (EVM_REFORMAT_DECIMALS.acala.includes(networkKey)) { + if (EVM_REFORMAT_DECIMALS.acala.includes(chain)) { const numberReplace = 18 - 12; transactionObject.value = transactionObject.value.substring(0, transactionObject.value.length - 6) + new Array(numberReplace).fill('0').join(''); @@ -61,16 +70,19 @@ export async function getEVMTransactionObject ( } export async function getERC20TransactionObject ( - assetAddress: string, - chainInfo: _ChainInfo, - from: string, - to: string, - value: string, - transferAll: boolean, - evmApi: _EvmApi + { assetAddress, + evmApi, + feeCustom: _feeCustom, + feeInfo: _feeInfo, + feeOption, + from, + to, + transferAll, + value }: TransferERC20Props ): Promise<[TransactionConfig, string]> { - const networkKey = chainInfo.slug; const erc20Contract = getERC20Contract(assetAddress, evmApi); + const feeCustom = _feeCustom as EvmEIP1559FeeOption; + let freeAmount = new BigN(0); let transferValue = value; @@ -88,14 +100,10 @@ export async function getERC20TransactionObject ( } const transferData = generateTransferData(to, transferValue); - const [gasLimit, priority] = await Promise.all([ - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - erc20Contract.methods.transfer(to, transferValue).estimateGas({ from }) - .catch(() => { - throw Error('Unable to estimate fee for this transaction. Try again or contact support at agent@subwallet.app'); - }) as number, - calculateGasFeeParams(evmApi, networkKey) - ]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + const gasLimit = await erc20Contract.methods.transfer(to, transferValue).estimateGas({ from }) as number; + const feeInfo = _feeInfo as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo, feeOption, feeCustom); const transactionObject = { gas: gasLimit, @@ -103,9 +111,7 @@ export async function getERC20TransactionObject ( value: '0', to: assetAddress, data: transferData, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; if (transferAll) { @@ -116,24 +122,34 @@ export async function getERC20TransactionObject ( return [transactionObject, transferValue]; } +interface TransferERC20Props extends TransactionFee { + assetAddress: string; + chain: string; + evmApi: _EvmApi; + from: string; + feeInfo: FeeInfo; + to: string; + transferAll: boolean; + value: string; +} + export async function getERC721Transaction ( web3Api: _EvmApi, chain: string, contractAddress: string, senderAddress: string, recipientAddress: string, - tokenId: string): Promise { + tokenId: string, + _feeInfo: FeeInfo): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const contract = new web3Api.api.eth.Contract(_ERC721_ABI, contractAddress); let gasLimit: number; - let priority: EvmFeeInfo; try { - [gasLimit, priority] = await Promise.all([ + [gasLimit] = await Promise.all([ // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - contract.methods.safeTransferFrom(senderAddress, recipientAddress, tokenId).estimateGas({ from: senderAddress }) as number, - calculateGasFeeParams(web3Api, chain) + contract.methods.safeTransferFrom(senderAddress, recipientAddress, tokenId).estimateGas({ from: senderAddress }) as number ]); } catch (e) { const error = e as Error; @@ -145,16 +161,17 @@ export async function getERC721Transaction ( throw error; } + const feeInfo = _feeInfo as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo); + return { from: senderAddress, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString(), gas: gasLimit, to: contractAddress, value: '0x00', // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - data: contract.methods.safeTransferFrom(senderAddress, recipientAddress, tokenId).encodeABI() + data: contract.methods.safeTransferFrom(senderAddress, recipientAddress, tokenId).encodeABI(), + ...feeCombine }; } diff --git a/packages/extension-base/src/services/balance-service/transfer/token.ts b/packages/extension-base/src/services/balance-service/transfer/token.ts index 79da9c13c41..c38dd96e705 100644 --- a/packages/extension-base/src/services/balance-service/transfer/token.ts +++ b/packages/extension-base/src/services/balance-service/transfer/token.ts @@ -10,7 +10,7 @@ import { _TRANSFER_CHAIN_GROUP } from '@subwallet/extension-base/services/chain- import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getContractAddressOfToken, _getTokenOnChainAssetId, _getTokenOnChainInfo, _getXcmAssetMultilocation, _isBridgedToken, _isChainEvmCompatible, _isChainTonCompatible, _isNativeToken, _isTokenGearSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon, _isTokenWasmSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; -import { getGRC20ContractPromise, getVFTContractPromise } from '@subwallet/extension-base/utils'; +import { combineEthFee, getGRC20ContractPromise, getVFTContractPromise } from '@subwallet/extension-base/utils'; import { keyring } from '@subwallet/ui-keyring'; import { internal } from '@ton/core'; import { Address } from '@ton/ton'; @@ -138,13 +138,14 @@ export const getTransferMockTxFee = async (address: string, chainInfo: _ChainInf }; const gasLimit = await web3.api.eth.estimateGas(transaction); const priority = await calculateGasFeeParams(web3, chainInfo.slug); + const combinedFee = combineEthFee(priority); - if (priority.baseGasFee) { - const maxFee = priority.maxFeePerGas; + if (combinedFee.maxFeePerGas) { + const maxFee = combinedFee.maxFeePerGas; - estimatedFee = maxFee.multipliedBy(gasLimit); + estimatedFee = BigN(maxFee).multipliedBy(gasLimit); } else { - estimatedFee = new BigN(priority.gasPrice).multipliedBy(gasLimit); + estimatedFee = new BigN(combinedFee.gasPrice as string).multipliedBy(gasLimit); } } else if (_isChainTonCompatible(chainInfo) && _isTokenTransferredByTon(tokenInfo)) { const mockWalletContract = keyring.getPair(address).ton.currentContract; diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts index 1253eac4964..b3ee74434b4 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts @@ -6,9 +6,10 @@ import { _ChainInfo } from '@subwallet/chain-list/types'; import { getWeb3Contract } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; import { _AVAIL_BRIDGE_GATEWAY_ABI, _AVAIL_TEST_BRIDGE_GATEWAY_ABI, getAvailBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { _NotificationInfo, ClaimAvailBridgeNotificationMetadata } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { AVAIL_BRIDGE_API } from '@subwallet/extension-base/services/inapp-notification-service/utils'; +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeCustom, FeeInfo, FeeOption } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { decodeAddress } from '@subwallet/keyring'; import { PrefixedHexString } from 'ethereumjs-util'; import { TransactionConfig } from 'web3-core'; @@ -54,7 +55,7 @@ type Message = { messageType: string; }; -export async function getAvailBridgeTxFromEth (originChainInfo: _ChainInfo, sender: string, recipient: string, value: string, evmApi: _EvmApi): Promise { +export async function getAvailBridgeTxFromEth (originChainInfo: _ChainInfo, sender: string, recipient: string, value: string, evmApi: _EvmApi, fee: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const availBridgeContractAddress = getAvailBridgeGatewayContract(originChainInfo.slug); const ABI = getAvailBridgeAbi(originChainInfo.slug); const availBridgeContract = getWeb3Contract(availBridgeContractAddress, evmApi, ABI); @@ -62,18 +63,19 @@ export async function getAvailBridgeTxFromEth (originChainInfo: _ChainInfo, send // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access const sendAvail = availBridgeContract.methods.sendAVAIL(_address, value) as ContractSendMethod; const transferData = sendAvail.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); const gasLimit = await sendAvail.estimateGas({ from: sender }); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = fee as EvmFeeInfo; + + const feeCombine = combineEthFee(feeInfo, feeOption, _feeCustom); return { from: sender, to: availBridgeContractAddress, value: '0', data: transferData, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString(), - gas: gasLimit + gas: gasLimit, + ...feeCombine } as TransactionConfig; } @@ -168,7 +170,7 @@ function getAvailBridgeApi (chainSlug: string) { return AVAIL_BRIDGE_API.AVAIL_TESTNET; } -export async function getClaimTxOnEthereum (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi) { +export async function getClaimTxOnEthereum (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi, feeInfo: EvmFeeInfo) { const availBridgeContractAddress = getAvailBridgeGatewayContract(chainSlug); const ABI = getAvailBridgeAbi(chainSlug); const availBridgeContract = getWeb3Contract(availBridgeContractAddress, evmApi, ABI); @@ -214,17 +216,16 @@ export async function getClaimTxOnEthereum (chainSlug: string, notification: _No ) as ContractSendMethod; const transferData = transfer.encodeABI(); const gasLimit = await transfer.estimateGas({ from: metadata.receiverAddress }); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + + const feeCombine = combineEthFee(feeInfo); return { from: metadata.receiverAddress, to: availBridgeContractAddress, value: '0', data: transferData, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString(), - gas: gasLimit + gas: gasLimit, + ...feeCombine } as TransactionConfig; } diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/index.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/index.ts index 6f6407d265e..30720c6a0ab 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/index.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/index.ts @@ -11,40 +11,41 @@ import { getExtrinsicByXcmPalletPallet } from '@subwallet/extension-base/service import { getExtrinsicByXtokensPallet } from '@subwallet/extension-base/services/balance-service/transfer/xcm/xTokens'; import { _XCM_CHAIN_GROUP } from '@subwallet/extension-base/services/chain-service/constants'; import { _EvmApi, _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; -import { _isChainEvmCompatible, _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; -import BigN from 'bignumber.js'; +import { _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; +import { FeeInfo, TransactionFee } from '@subwallet/extension-base/types'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { u8aToHex } from '@polkadot/util'; -import { addressToEvm } from '@polkadot/util-crypto'; -import { _createPosBridgeL1toL2Extrinsic, _createPosBridgeL2toL1Extrinsic, _isPosChainBridge } from './posBridge'; +import { _createPosBridgeL1toL2Extrinsic, _createPosBridgeL2toL1Extrinsic } from './posBridge'; export type CreateXcmExtrinsicProps = { - originTokenInfo: _ChainAsset; + destinationChain: _ChainInfo; destinationTokenInfo: _ChainAsset; + evmApi?: _EvmApi; + originChain: _ChainInfo; + originTokenInfo: _ChainAsset; recipient: string; + sender: string; sendingValue: string; - evmApi?: _EvmApi; substrateApi?: _SubstrateApi; - chainInfoMap: Record; - sender?: string; -} + feeInfo: FeeInfo; +} & TransactionFee; export type FunctionCreateXcmExtrinsic = (props: CreateXcmExtrinsicProps) => Promise | TransactionConfig>; -export const createSnowBridgeExtrinsic = async ({ chainInfoMap, - destinationTokenInfo, +// SnowBridge +export const createSnowBridgeExtrinsic = async ({ destinationChain, evmApi, + feeCustom, + feeInfo, + feeOption, + originChain, originTokenInfo, recipient, sender, sendingValue }: CreateXcmExtrinsicProps): Promise => { - const originChainInfo = chainInfoMap[originTokenInfo.originChain]; - const destinationChainInfo = chainInfoMap[destinationTokenInfo.originChain]; - - if (!_isSnowBridgeXcm(originChainInfo, destinationChainInfo)) { + if (!_isSnowBridgeXcm(originChain, destinationChain)) { throw new Error('This is not a valid SnowBridge transfer'); } @@ -56,18 +57,15 @@ export const createSnowBridgeExtrinsic = async ({ chainInfoMap, throw Error('Sender is required'); } - return getSnowBridgeEvmTransfer(originTokenInfo, originChainInfo, destinationChainInfo, sender, recipient, sendingValue, evmApi); + return getSnowBridgeEvmTransfer(originTokenInfo, originChain, destinationChain, sender, recipient, sendingValue, evmApi, feeInfo, feeCustom, feeOption); }; -export const createXcmExtrinsic = async ({ chainInfoMap, - destinationTokenInfo, +export const createXcmExtrinsic = async ({ destinationChain, + originChain, originTokenInfo, recipient, sendingValue, substrateApi }: CreateXcmExtrinsicProps): Promise> => { - const originChainInfo = chainInfoMap[originTokenInfo.originChain]; - const destinationChainInfo = chainInfoMap[destinationTokenInfo.originChain]; - if (!substrateApi) { throw Error('Substrate API is not available'); } @@ -75,27 +73,27 @@ export const createXcmExtrinsic = async ({ chainInfoMap, const chainApi = await substrateApi.isReady; const api = chainApi.api; - const polkadotXcmSpecialCases = _XCM_CHAIN_GROUP.polkadotXcmSpecialCases.includes(originChainInfo.slug) && _isNativeToken(originTokenInfo); + const polkadotXcmSpecialCases = _XCM_CHAIN_GROUP.polkadotXcmSpecialCases.includes(originChain.slug) && _isNativeToken(originTokenInfo); if (_XCM_CHAIN_GROUP.polkadotXcm.includes(originTokenInfo.originChain) || polkadotXcmSpecialCases) { - return getExtrinsicByPolkadotXcmPallet(originTokenInfo, originChainInfo, destinationChainInfo, recipient, sendingValue, api); + return getExtrinsicByPolkadotXcmPallet(originTokenInfo, originChain, destinationChain, recipient, sendingValue, api); } if (_XCM_CHAIN_GROUP.xcmPallet.includes(originTokenInfo.originChain)) { - return getExtrinsicByXcmPalletPallet(originTokenInfo, originChainInfo, destinationChainInfo, recipient, sendingValue, api); + return getExtrinsicByXcmPalletPallet(originTokenInfo, originChain, destinationChain, recipient, sendingValue, api); } - return getExtrinsicByXtokensPallet(originTokenInfo, originChainInfo, destinationChainInfo, recipient, sendingValue, api); + return getExtrinsicByXtokensPallet(originTokenInfo, originChain, destinationChain, recipient, sendingValue, api); }; -export const createAvailBridgeTxFromEth = ({ chainInfoMap, - evmApi, - originTokenInfo, +export const createAvailBridgeTxFromEth = ({ evmApi, + feeCustom, + feeInfo, + feeOption, + originChain, recipient, sender, sendingValue }: CreateXcmExtrinsicProps): Promise => { - const originChainInfo = chainInfoMap[originTokenInfo.originChain]; - if (!evmApi) { throw Error('Evm API is not available'); } @@ -104,7 +102,7 @@ export const createAvailBridgeTxFromEth = ({ chainInfoMap, throw Error('Sender is required'); } - return getAvailBridgeTxFromEth(originChainInfo, sender, recipient, sendingValue, evmApi); + return getAvailBridgeTxFromEth(originChain, sender, recipient, sendingValue, evmApi, feeInfo, feeCustom, feeOption); }; export const createAvailBridgeExtrinsicFromAvail = async ({ recipient, sendingValue, substrateApi }: CreateXcmExtrinsicProps): Promise> => { @@ -115,18 +113,19 @@ export const createAvailBridgeExtrinsicFromAvail = async ({ recipient, sendingVa return await getAvailBridgeExtrinsicFromAvail(recipient, sendingValue, substrateApi); }; -export const createPolygonBridgeExtrinsic = async ({ chainInfoMap, - destinationTokenInfo, +export const createPolygonBridgeExtrinsic = async ({ destinationChain, evmApi, + feeCustom, + feeInfo, + feeOption, + originChain, originTokenInfo, recipient, sender, sendingValue }: CreateXcmExtrinsicProps): Promise => { - const originChainInfo = chainInfoMap[originTokenInfo.originChain]; - const destinationChainInfo = chainInfoMap[destinationTokenInfo.originChain]; - const isPolygonBridgeXcm = _isPolygonBridgeXcm(originChainInfo, destinationChainInfo); + const isPolygonBridgeXcm = _isPolygonBridgeXcm(originChain, destinationChain); - const isValidBridge = isPolygonBridgeXcm || _isPosBridgeXcm(originChainInfo, destinationChainInfo); + const isValidBridge = isPolygonBridgeXcm || _isPosBridgeXcm(originChain, destinationChain); if (!isValidBridge) { throw new Error('This is not a valid PolygonBridge transfer'); @@ -140,7 +139,7 @@ export const createPolygonBridgeExtrinsic = async ({ chainInfoMap, throw Error('Sender is required'); } - const sourceChain = originChainInfo.slug; + const sourceChain = originChain.slug; const createExtrinsic = isPolygonBridgeXcm ? (sourceChain === 'polygonzkEvm_cardona' || sourceChain === 'polygonZkEvm') @@ -150,36 +149,5 @@ export const createPolygonBridgeExtrinsic = async ({ chainInfoMap, ? _createPosBridgeL2toL1Extrinsic : _createPosBridgeL1toL2Extrinsic; - return createExtrinsic(originTokenInfo, originChainInfo, sender, recipient, sendingValue, evmApi); -}; - -export const getXcmMockTxFee = async (substrateApi: _SubstrateApi, chainInfoMap: Record, originTokenInfo: _ChainAsset, destinationTokenInfo: _ChainAsset): Promise => { - try { - const destChainInfo = chainInfoMap[destinationTokenInfo.originChain]; - const originChainInfo = chainInfoMap[originTokenInfo.originChain]; - const fakeAddress = '5DRewsYzhJqZXU3SRaWy1FSt5iDr875ao91aw5fjrJmDG4Ap'; // todo: move this - const substrateAddress = fakeAddress; // todo: move this - const evmAddress = u8aToHex(addressToEvm(fakeAddress)); // todo: move this - - // mock receiving account from sender - const sender = _isChainEvmCompatible(originChainInfo) ? evmAddress : substrateAddress; - const recipient = _isChainEvmCompatible(destChainInfo) ? evmAddress : substrateAddress; - - const mockTx = await createXcmExtrinsic({ - chainInfoMap, - destinationTokenInfo, - originTokenInfo, - sender, - recipient, - sendingValue: '1000000000000000000', - substrateApi - }); - const paymentInfo = await mockTx.paymentInfo(fakeAddress); - - return new BigN(paymentInfo?.partialFee?.toString() || '0'); - } catch (e) { - console.error('error mocking xcm tx fee', e); - - return new BigN(0); - } + return createExtrinsic(originTokenInfo, originChain, sender, recipient, sendingValue, evmApi, feeInfo, feeCustom, feeOption); }; diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/polygonBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/polygonBridge.ts index e8d2fc5f927..8f650cb8c0b 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/polygonBridge.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/polygonBridge.ts @@ -7,8 +7,9 @@ import { getWeb3Contract } from '@subwallet/extension-base/koni/api/contract-han import { _POLYGON_BRIDGE_ABI, getPolygonBridgeContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getContractAddressOfToken } from '@subwallet/extension-base/services/chain-service/utils'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { _NotificationInfo, ClaimPolygonBridgeNotificationMetadata } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeCustom, FeeInfo, FeeOption } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { TransactionConfig } from 'web3-core'; import { ContractSendMethod } from 'web3-eth-contract'; @@ -39,7 +40,18 @@ export const POLYGON_GAS_INDEXER = { TESTNET: 'https://gasstation.polygon.technology/zkevm/cardona' }; -async function createPolygonBridgeTransaction (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, destinationNetwork: number, evmApi: _EvmApi): Promise { +async function createPolygonBridgeTransaction ( + tokenInfo: _ChainAsset, + originChainInfo: _ChainInfo, + sender: string, + recipientAddress: string, + value: string, + destinationNetwork: number, + evmApi: _EvmApi, + _feeInfo: FeeInfo, + feeCustom?: FeeCustom, + feeOption?: FeeOption +): Promise { const polygonBridgeContractAddress = getPolygonBridgeContract(originChainInfo.slug); const polygonBridgeContract = getWeb3Contract(polygonBridgeContractAddress, evmApi, _POLYGON_BRIDGE_ABI); const tokenContract = _getContractAddressOfToken(tokenInfo) || '0x0000000000000000000000000000000000000000'; // FOR Ethereum: use null address @@ -58,16 +70,16 @@ async function createPolygonBridgeTransaction (tokenInfo: _ChainAsset, originCha '0x' ); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + const feeInfo = _feeInfo as EvmFeeInfo; + + const feeCombine = combineEthFee(feeInfo, feeOption, feeCustom as EvmEIP1559FeeOption); const transactionConfig: TransactionConfig = { from: sender, to: polygonBridgeContractAddress, value: value, data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority?.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority?.maxPriorityFeePerGas?.toString() + ...feeCombine }; const gasLimit = await evmApi.api.eth.estimateGas(transactionConfig).catch(() => 200000); @@ -77,15 +89,15 @@ async function createPolygonBridgeTransaction (tokenInfo: _ChainAsset, originCha return transactionConfig; } -export async function _createPolygonBridgeL1toL2Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi): Promise { - return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 1, evmApi); +export async function _createPolygonBridgeL1toL2Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi, feeInfo: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { + return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 1, evmApi, feeInfo, feeCustom, feeOption); } -export async function _createPolygonBridgeL2toL1Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi): Promise { - return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 0, evmApi); +export async function _createPolygonBridgeL2toL1Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi, feeInfo: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { + return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 0, evmApi, feeInfo, feeCustom, feeOption); } -export async function getClaimPolygonBridge (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi) { +export async function getClaimPolygonBridge (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi, feeInfo: EvmFeeInfo) { const polygonBridgeContractAddress = getPolygonBridgeContract(chainSlug); const polygonBridgeContract = getWeb3Contract(polygonBridgeContractAddress, evmApi, _POLYGON_BRIDGE_ABI); const metadata = notification.metadata as ClaimPolygonBridgeNotificationMetadata; @@ -101,16 +113,14 @@ export async function getClaimPolygonBridge (chainSlug: string, notification: _N const transferCall: ContractSendMethod = polygonBridgeContract.methods.claimAsset(proof.merkle_proof, proof.rollup_merkle_proof, metadata.counter, proof.main_exit_root, proof.rollup_exit_root, metadata.originTokenNetwork, metadata.originTokenAddress, metadata.destinationNetwork, metadata.receiver, metadata.amounts[0], '0x'); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + const feeCombine = combineEthFee(feeInfo); const transactionConfig = { from: metadata.userAddress, to: polygonBridgeContractAddress, value: '0', data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; const gasLimit = await evmApi.api.eth.estimateGas(transactionConfig).catch(() => 200000); diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/posBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/posBridge.ts index 4b64c454374..273ee52735c 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/posBridge.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/posBridge.ts @@ -6,10 +6,10 @@ import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; import { getWeb3Contract } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; import { _POS_BRIDGE_ABI, _POS_BRIDGE_L2_ABI, getPosL1BridgeContract, getPosL2BridgeContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { _NotificationInfo, ClaimPolygonBridgeNotificationMetadata } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { fetchPolygonBridgeTransactions } from '@subwallet/extension-base/services/inapp-notification-service/utils'; -import { BasicTxErrorType } from '@subwallet/extension-base/types'; +import { BasicTxErrorType, EvmEIP1559FeeOption, EvmFeeInfo, FeeCustom, FeeInfo, FeeOption } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { TransactionConfig } from 'web3-core'; import { ContractSendMethod } from 'web3-eth-contract'; @@ -32,23 +32,26 @@ export const POS_EXIT_PAYLOAD_INDEXER = { TESTNET: 'https://proof-generator.polygon.technology/api/v1/amoy/exit-payload' }; -export async function _createPosBridgeL1toL2Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi): Promise { +export async function _createPosBridgeL1toL2Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi, _feeInfo: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const posBridgeContractAddress = getPosL1BridgeContract(originChainInfo.slug); const posBridgeContract = getWeb3Contract(posBridgeContractAddress, evmApi, _POS_BRIDGE_ABI); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = _feeInfo as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo, feeOption, _feeCustom); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment const transferCall: ContractSendMethod = posBridgeContract.methods.depositEtherFor(recipientAddress); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + + // const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); const transactionConfig: TransactionConfig = { from: sender, to: posBridgeContractAddress, value: value, data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority?.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority?.maxPriorityFeePerGas?.toString() + ...feeCombine }; const gasLimit = await evmApi.api.eth.estimateGas(transactionConfig).catch(() => 200000); @@ -58,23 +61,24 @@ export async function _createPosBridgeL1toL2Extrinsic (tokenInfo: _ChainAsset, o return transactionConfig; } -export async function _createPosBridgeL2toL1Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi): Promise { +export async function _createPosBridgeL2toL1Extrinsic (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi, _feeInfo: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const posBridgeContractAddress = getPosL2BridgeContract(originChainInfo.slug); const posBridgeContract = getWeb3Contract(posBridgeContractAddress, evmApi, _POS_BRIDGE_L2_ABI); // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment const transferCall: ContractSendMethod = posBridgeContract.methods.withdraw(value); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = _feeInfo as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo, feeOption, _feeCustom); const transactionConfig: TransactionConfig = { from: sender, to: posBridgeContractAddress, value: undefined, data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority?.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority?.maxPriorityFeePerGas?.toString() + ...feeCombine }; const gasLimit = await evmApi.api.eth.estimateGas(transactionConfig).catch(() => 200000); @@ -84,7 +88,7 @@ export async function _createPosBridgeL2toL1Extrinsic (tokenInfo: _ChainAsset, o return transactionConfig; } -export async function getClaimPosBridge (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi) { +export async function getClaimPosBridge (chainSlug: string, notification: _NotificationInfo, evmApi: _EvmApi, feeInfo: EvmFeeInfo) { const posBridgeContractAddress = getPosL2BridgeContract(chainSlug); const posBridgeContract = getWeb3Contract(posBridgeContractAddress, evmApi, _POS_BRIDGE_L2_ABI); @@ -120,16 +124,14 @@ export async function getClaimPosBridge (chainSlug: string, notification: _Notif const transferCall: ContractSendMethod = posClaimContract.methods.exit(inputData.result); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + const feeCombine = combineEthFee(feeInfo); const transactionConfig = { from: metadata.userAddress, to: posClaimContractAddress, value: '0', data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; const gasLimit = await evmApi.api.eth.estimateGas(transactionConfig).catch(() => 200000); diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/snowBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/snowBridge.ts index 72340063647..91ceef2a299 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/snowBridge.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/snowBridge.ts @@ -7,7 +7,8 @@ import { getWeb3Contract } from '@subwallet/extension-base/koni/api/contract-han import { _SNOWBRIDGE_GATEWAY_ABI, getSnowBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getContractAddressOfToken, _getSubstrateParaId, _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; -import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeCustom, FeeInfo, FeeOption } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { TransactionConfig } from 'web3-core'; import { Contract } from 'web3-eth-contract'; @@ -22,7 +23,7 @@ async function getSendFeeToken (contract: Contract, tokenContract: _Address, des return (await quoteSendTokenFee.call()) as string; } -export async function getSnowBridgeEvmTransfer (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, destinationChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi): Promise { +export async function getSnowBridgeEvmTransfer (tokenInfo: _ChainAsset, originChainInfo: _ChainInfo, destinationChainInfo: _ChainInfo, sender: string, recipientAddress: string, value: string, evmApi: _EvmApi, _feeInfo: FeeInfo, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const snowBridgeContractAddress = getSnowBridgeGatewayContract(originChainInfo.slug); const snowBridgeContract = getWeb3Contract(snowBridgeContractAddress, evmApi, _SNOWBRIDGE_GATEWAY_ABI); const tokenContract = _getContractAddressOfToken(tokenInfo); @@ -38,19 +39,18 @@ export async function getSnowBridgeEvmTransfer (tokenInfo: _ChainAsset, originCh // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment const transferEncodedCall = transferCall.encodeABI() as string; - const [priority, sendTokenFee] = await Promise.all([ - calculateGasFeeParams(evmApi, evmApi.chainSlug), - getSendFeeToken(snowBridgeContract, tokenContract, destinationChainParaId, destinationFee) - ]); + const feeInfo = _feeInfo as EvmFeeInfo; + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeCombine = combineEthFee(feeInfo, feeOption, _feeCustom); + + const sendTokenFee = await getSendFeeToken(snowBridgeContract, tokenContract, destinationChainParaId, destinationFee); const transactionConfig = { from: sender, to: snowBridgeContractAddress, value: sendTokenFee, data: transferEncodedCall, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; let gasLimit; diff --git a/packages/extension-base/src/services/earning-service/handlers/liquid-staking/stella-swap.ts b/packages/extension-base/src/services/earning-service/handlers/liquid-staking/stella-swap.ts index 88adc91f88f..4834615f974 100644 --- a/packages/extension-base/src/services/earning-service/handlers/liquid-staking/stella-swap.ts +++ b/packages/extension-base/src/services/earning-service/handlers/liquid-staking/stella-swap.ts @@ -9,6 +9,7 @@ import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types' import { _getAssetDecimals, _getContractAddressOfToken } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import { BaseYieldStepDetail, BasicTxErrorType, EarningStatus, HandleYieldStepData, LiquidYieldPoolInfo, OptimalYieldPath, OptimalYieldPathParams, SubmitYieldJoinData, TokenSpendingApprovalParams, TransactionData, UnstakingInfo, UnstakingStatus, YieldPoolMethodInfo, YieldPositionInfo, YieldStepType, YieldTokenBaseInfo } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; import { TransactionConfig } from 'web3-core'; import { Contract } from 'web3-eth-contract'; @@ -347,15 +348,14 @@ export default class StellaSwapLiquidStakingPoolHandler extends BaseLiquidStakin // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access const gasLimit = await depositCall.estimateGas({ from: address }) as number; const priority = await calculateGasFeeParams(evmApi, this.chain); + const feeCombine = combineEthFee(priority); const transactionObject = { from: address, to: _getContractAddressOfToken(derivativeTokenInfo), data: depositEncodedCall, gas: gasLimit, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; return { @@ -391,15 +391,14 @@ export default class StellaSwapLiquidStakingPoolHandler extends BaseLiquidStakin // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access const gasLimit = await redeemCall.estimateGas({ from: address }) as number; const priority = await calculateGasFeeParams(evmApi, this.chain); + const feeCombine = combineEthFee(priority); const transaction: TransactionConfig = { from: address, to: _getContractAddressOfToken(derivativeTokenInfo), data: redeemEncodedCall, gas: gasLimit, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine }; return [ExtrinsicType.UNSTAKE_STDOT, transaction]; @@ -424,15 +423,14 @@ export default class StellaSwapLiquidStakingPoolHandler extends BaseLiquidStakin // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access const gasLimit = await withdrawCall.estimateGas({ from: address }) as number; const priority = await calculateGasFeeParams(evmApi, this.chain); + const feeCombine = combineEthFee(priority); return { from: address, to: _getContractAddressOfToken(derivativeTokenInfo), data: withdrawEncodedCall, gas: gasLimit, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine }; // TODO: check tx history parsing } diff --git a/packages/extension-base/src/services/earning-service/handlers/special.ts b/packages/extension-base/src/services/earning-service/handlers/special.ts index cfbb4d994e5..3265971537f 100644 --- a/packages/extension-base/src/services/earning-service/handlers/special.ts +++ b/packages/extension-base/src/services/earning-service/handlers/special.ts @@ -10,6 +10,7 @@ import { createXcmExtrinsic } from '@subwallet/extension-base/services/balance-s import { _getAssetDecimals, _getAssetExistentialDeposit, _getAssetName, _getAssetSymbol, _getChainNativeTokenSlug } from '@subwallet/extension-base/services/chain-service/utils'; import { BaseYieldStepDetail, BasicTxErrorType, HandleYieldStepData, OptimalYieldPath, OptimalYieldPathParams, RequestCrossChainTransfer, RequestEarlyValidateYield, ResponseEarlyValidateYield, RuntimeDispatchInfo, SpecialYieldPoolInfo, SpecialYieldPoolMetadata, SubmitYieldJoinData, SubmitYieldStepData, TransactionData, UnstakingInfo, YieldPoolInfo, YieldPoolTarget, YieldPoolType, YieldProcessValidation, YieldStepBaseInfo, YieldStepType, YieldTokenBaseInfo, YieldValidationStatus } from '@subwallet/extension-base/types'; import { createPromiseHandler, formatNumber, PromiseHandler } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { t } from 'i18next'; import { BN, BN_TEN, BN_ZERO, noop } from '@polkadot/util'; @@ -279,14 +280,19 @@ export default abstract class BaseSpecialStakingPoolHandler extends BasePoolHand }; const xcmOriginSubstrateApi = await this.state.getSubstrateApi(altInputTokenInfo.originChain).isReady; + const id = getId(); + const feeInfo = await this.state.feeService.subscribeChainFee(id, altChainInfo.slug, 'substrate'); const xcmTransfer = await createXcmExtrinsic({ + sender: address, originTokenInfo: altInputTokenInfo, destinationTokenInfo: inputTokenInfo, sendingValue: bnAmount.toString(), recipient: address, - chainInfoMap: this.state.getChainInfoMap(), - substrateApi: xcmOriginSubstrateApi + destinationChain: this.chainInfo, + originChain: altChainInfo, + substrateApi: xcmOriginSubstrateApi, + feeInfo }); const _xcmFeeInfo = await xcmTransfer.paymentInfo(address); @@ -534,13 +540,19 @@ export default abstract class BaseSpecialStakingPoolHandler extends BasePoolHand const bnTotalAmount = bnAmount.sub(bnInputTokenBalance).add(bnXcmFee); + const id = getId(); + const feeInfo = await this.state.feeService.subscribeChainFee(id, originChainInfo.slug, 'substrate'); + const extrinsic = await createXcmExtrinsic({ - chainInfoMap: this.state.getChainInfoMap(), destinationTokenInfo, originTokenInfo, recipient: address, sendingValue: bnTotalAmount.toString(), - substrateApi + substrateApi, + sender: address, + originChain: originChainInfo, + destinationChain: this.chainInfo, + feeInfo }); const xcmData: RequestCrossChainTransfer = { diff --git a/packages/extension-base/src/services/fee-service/interfaces.ts b/packages/extension-base/src/services/fee-service/interfaces.ts new file mode 100644 index 00000000000..4deb8f32771 --- /dev/null +++ b/packages/extension-base/src/services/fee-service/interfaces.ts @@ -0,0 +1,7 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export interface TokenHasBalanceInfo { + slug: string; + free: string; +} diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index 40059860d7b..97c73c0f395 100644 --- a/packages/extension-base/src/services/fee-service/service.ts +++ b/packages/extension-base/src/services/fee-service/service.ts @@ -4,7 +4,7 @@ import KoniState from '@subwallet/extension-base/koni/background/handlers/State'; import { _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; -import { EvmFeeInfo } from '@subwallet/extension-base/types'; +import { EvmFeeInfo, FeeChainType, FeeInfo, FeeSubscription } from '@subwallet/extension-base/types'; import { BehaviorSubject } from 'rxjs'; export default class FeeService { @@ -13,6 +13,12 @@ export default class FeeService { private evmFeeSubject: BehaviorSubject> = new BehaviorSubject>({}); private useInfura: boolean; + private chainFeeSubscriptionMap: Record> = { + evm: {}, + substrate: {}, + ton: {} + }; + constructor (state: KoniState) { this.state = state; this.useInfura = true; @@ -82,4 +88,130 @@ export default class FeeService { clearInterval(interval); }; } + + public subscribeChainFee (id: string, chain: string, type: FeeChainType, callback?: (data: FeeInfo) => void) { + return new Promise((resolve) => { + const _callback = (value: FeeInfo | undefined) => { + console.log(id, this.chainFeeSubscriptionMap); + + if (value) { + callback?.(value); + resolve(value); + } + }; + + const feeSubscription = this.chainFeeSubscriptionMap[type][chain]; + + if (feeSubscription) { + const observer = feeSubscription.observer; + + _callback(observer.getValue()); + + // If have callback, just subscribe + if (callback) { + const subscription = observer.subscribe({ + next: _callback + }); + + this.chainFeeSubscriptionMap[type][chain].subscription[id] = () => { + if (!subscription.closed) { + subscription.unsubscribe(); + } + }; + } + } else { + const observer = new BehaviorSubject(undefined); + + const subscription = observer.subscribe({ + next: _callback + }); + + let cancel = false; + let interval: NodeJS.Timer; + + const update = () => { + if (cancel) { + clearInterval(interval); + } else { + const api = this.state.getEvmApi(chain); + + if (api) { + calculateGasFeeParams(api, chain) + .then((info) => { + observer.next(info); + }) + .catch((e) => { + console.warn(`Cannot get fee param for ${chain}`, e); + observer.next({ + type: 'evm', + gasPrice: '0', + baseGasFee: undefined, + options: undefined + } as EvmFeeInfo); + }); + } else { + observer.next({ + type: type as Exclude, + busyNetwork: false, + options: { + slow: { + tip: '0' + }, + average: { + tip: '0' + }, + fast: { + tip: '0' + }, + default: 'slow' + } + }); + clearInterval(interval); + } + } + }; + + update(); + + // If have callback, just subscribe + if (callback) { + interval = setInterval(update, 15 * 1000); + + const unsub = () => { + cancel = true; + observer.complete(); + clearInterval(interval); + }; + + this.chainFeeSubscriptionMap[type][chain] = { + observer, + subscription: { + [id]: () => { + if (!subscription.closed) { + subscription.unsubscribe(); + } + } + }, + unsubscribe: unsub + }; + } + } + }); + } + + public unsubscribeChainFee (id: string, chain: string, type: FeeChainType) { + const subscription = this.chainFeeSubscriptionMap[type][chain]; + + if (subscription) { + const unsub = subscription.subscription[id]; + + unsub && unsub(); + delete subscription.subscription[id]; + + if (Object.keys(subscription.subscription).length === 0) { + subscription.unsubscribe(); + delete this.chainFeeSubscriptionMap[type][chain]; + } + } + } } diff --git a/packages/extension-base/src/services/fee-service/utils/index.ts b/packages/extension-base/src/services/fee-service/utils/index.ts index d22dccea378..394ced162ba 100644 --- a/packages/extension-base/src/services/fee-service/utils/index.ts +++ b/packages/extension-base/src/services/fee-service/utils/index.ts @@ -1,56 +1,87 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { _ChainAsset } from '@subwallet/chain-list/types'; import { GAS_PRICE_RATIO, NETWORK_MULTI_GAS_FEE } from '@subwallet/extension-base/constants'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; -import { EvmFeeInfo, EvmFeeInfoCache, InfuraFeeInfo } from '@subwallet/extension-base/types'; +import { estimateTokensForPool, getReserveForPool } from '@subwallet/extension-base/services/swap-service/handler/asset-hub/utils'; +import { EvmEIP1559FeeOption, EvmFeeInfo, EvmFeeInfoCache, InfuraFeeInfo, InfuraThresholdInfo } from '@subwallet/extension-base/types'; import { BN_WEI, BN_ZERO } from '@subwallet/extension-base/utils'; import BigN from 'bignumber.js'; +import { ApiPromise } from '@polkadot/api'; + import { gasStation, POLYGON_GAS_INDEXER } from '../../balance-service/transfer/xcm/polygonBridge'; const INFURA_API_KEY = process.env.INFURA_API_KEY || ''; const INFURA_API_KEY_SECRET = process.env.INFURA_API_KEY_SECRET || ''; const INFURA_AUTH = 'Basic ' + Buffer.from(INFURA_API_KEY + ':' + INFURA_API_KEY_SECRET).toString('base64'); -export const parseInfuraFee = (info: InfuraFeeInfo): EvmFeeInfo => { +export const FEE_COVERAGE_PERCENTAGE_SPECIAL_CASE = 105; // percentage + +export const parseInfuraFee = (info: InfuraFeeInfo, threshold: InfuraThresholdInfo): EvmFeeInfo => { const base = new BigN(info.estimatedBaseFee).multipliedBy(BN_WEI); - const low = new BigN(info.low.suggestedMaxPriorityFeePerGas).multipliedBy(BN_WEI); - const busyNetwork = base.gt(BN_ZERO) ? low.dividedBy(base).gte(0.3) : false; - const data = !busyNetwork ? info.low : info.medium; + const thresholdBN = new BigN(threshold.busyThreshold).multipliedBy(BN_WEI); + const busyNetwork = thresholdBN.gte(BN_ZERO) ? base.gt(thresholdBN) : false; return { busyNetwork, gasPrice: undefined, - baseGasFee: base, - maxFeePerGas: new BigN(data.suggestedMaxFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP), - maxPriorityFeePerGas: new BigN(data.suggestedMaxPriorityFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP) + baseGasFee: base.toFixed(0), + type: 'evm', + options: { + slow: { + maxFeePerGas: new BigN(info.low.suggestedMaxFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxPriorityFeePerGas: new BigN(info.low.suggestedMaxPriorityFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxWaitTimeEstimate: info.low.maxWaitTimeEstimate || 0, + minWaitTimeEstimate: info.low.minWaitTimeEstimate || 0 + }, + average: { + maxFeePerGas: new BigN(info.medium.suggestedMaxFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxPriorityFeePerGas: new BigN(info.medium.suggestedMaxPriorityFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxWaitTimeEstimate: info.medium.maxWaitTimeEstimate || 0, + minWaitTimeEstimate: info.medium.minWaitTimeEstimate || 0 + }, + fast: { + maxFeePerGas: new BigN(info.high.suggestedMaxFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxPriorityFeePerGas: new BigN(info.high.suggestedMaxPriorityFeePerGas).multipliedBy(BN_WEI).integerValue(BigN.ROUND_UP).toFixed(0), + maxWaitTimeEstimate: info.high.maxWaitTimeEstimate || 0, + minWaitTimeEstimate: info.high.minWaitTimeEstimate || 0 + }, + default: busyNetwork ? 'average' : 'slow' + } }; }; export const fetchInfuraFeeData = async (chainId: number, infuraAuth?: string): Promise => { - return await new Promise((resolve) => { - const baseUrl = 'https://gas.api.infura.io/networks/{{chainId}}/suggestedGasFees'; - const url = baseUrl.replaceAll('{{chainId}}', chainId.toString()); + const baseUrl = 'https://gas.api.infura.io/networks/{{chainId}}/suggestedGasFees'; + const baseThressholdUrl = 'https://gas.api.infura.io/networks/{{chainId}}/busyThreshold'; + // const baseFeeHistoryUrl = 'https://gas.api.infura.io/networks/{{chainId}}/baseFeeHistory'; + // const baseFeePercentileUrl = 'https://gas.api.infura.io/networks/{{chainId}}/baseFeePercentile'; + const feeUrl = baseUrl.replaceAll('{{chainId}}', chainId.toString()); + const thressholdUrl = baseThressholdUrl.replaceAll('{{chainId}}', chainId.toString()); - fetch(url, - { + try { + const [feeResp, thressholdResp] = await Promise.all([feeUrl, thressholdUrl].map((url) => { + return fetch(url, { method: 'GET', headers: { - Authorization: infuraAuth || INFURA_AUTH + Authorization: INFURA_AUTH } - }) - .then((rs) => { - return rs.json(); - }) - .then((info: InfuraFeeInfo) => { - resolve(parseInfuraFee(info)); - }) - .catch((e) => { - console.warn(e); - resolve(null); }); - }); + })); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [feeInfo, thresholdInfo]: [InfuraFeeInfo, InfuraThresholdInfo] = await Promise.all([ + feeResp.json(), + thressholdResp.json()]); + + return parseInfuraFee(feeInfo, thresholdInfo); + } catch (e) { + console.warn(e); + + return null; + } }; export const fetchSubWalletFeeData = async (chainId: number, networkKey: string): Promise => { @@ -58,6 +89,7 @@ export const fetchSubWalletFeeData = async (chainId: number, networkKey: string) const baseUrl = 'https://api-cache.subwallet.app/sw-evm-gas/{{chain}}'; const url = baseUrl.replaceAll('{{chain}}', networkKey); + // TODO: Update the logo to follow the new estimateFee format or move the logic to the backend fetch(url, { method: 'GET' @@ -66,17 +98,7 @@ export const fetchSubWalletFeeData = async (chainId: number, networkKey: string) return rs.json(); }) .then((info: EvmFeeInfoCache) => { - if (info.gasPrice !== undefined) { - resolve(info); - } else { - resolve({ - busyNetwork: info.busyNetwork, - gasPrice: info.gasPrice, - baseGasFee: new BigN(info.baseGasFee), - maxFeePerGas: new BigN(info.maxFeePerGas), - maxPriorityFeePerGas: new BigN(info.maxPriorityFeePerGas) - }); - } + resolve(info); }) .catch((e) => { console.warn(e); @@ -106,10 +128,28 @@ export const recalculateGasPrice = (_price: string, chain: string) => { return needMulti ? new BigN(_price).multipliedBy(GAS_PRICE_RATIO).toFixed(0) : _price; }; -export const calculateGasFeeParams = async (web3: _EvmApi, networkKey: string, useOnline = true, useInfura = false): Promise => { +export const getEIP1559GasFee = ( + baseFee: BigN, + maxPriorityFee: BigN, + blockNumber: number, + blockTime: number +): EvmEIP1559FeeOption => { + // https://www.blocknative.com/blog/eip-1559-fees + const maxFee = baseFee.multipliedBy(1.2).plus(maxPriorityFee); + + return { + maxFeePerGas: maxFee.toFixed(0), + maxPriorityFeePerGas: maxPriorityFee.toFixed(0), + minWaitTimeEstimate: blockTime * (blockNumber - 2), + maxWaitTimeEstimate: blockTime * blockNumber + }; +}; + +export const calculateGasFeeParams = async (web3: _EvmApi, networkKey: string, useOnline = true, useInfura = true): Promise => { if (useOnline) { try { const chainId = await web3.api.eth.getChainId(); + const onlineData = await fetchOnlineFeeData(chainId, networkKey, useInfura); if (onlineData) { @@ -128,23 +168,25 @@ export const calculateGasFeeParams = async (web3: _EvmApi, networkKey: string, u const gasPriceInWei = gasResponse.standard * 1e9 + 200000; return { + type: 'evm', gasPrice: gasPriceInWei.toString(), - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, baseGasFee: undefined, - busyNetwork: false + busyNetwork: false, + options: undefined }; } const numBlock = 20; - const rewardPercent: number[] = []; - - for (let i = 0; i <= 100; i = i + 5) { - rewardPercent.push(i); - } + const rewardPercent: number[] = [25, 50, 75]; const history = await web3.api.eth.getFeeHistory(numBlock, 'latest', rewardPercent); + const currentBlock = history.oldestBlock - 1; + const [newBlock, oldBlock] = await Promise.all([ + web3.api.eth.getBlock(currentBlock), + web3.api.eth.getBlock(currentBlock - numBlock) + ]); + const blockTime = Number((BigInt(newBlock.timestamp || 0) - BigInt(oldBlock.timestamp || 0)) / BigInt(numBlock) * BigInt(1000)); const baseGasFee = new BigN(history.baseFeePerGas[history.baseFeePerGas.length - 1]); // Last element is latest const blocksBusy = history.reward.reduce((previous: number, rewards, currentIndex) => { @@ -166,93 +208,38 @@ export const calculateGasFeeParams = async (web3: _EvmApi, networkKey: string, u const busyNetwork = blocksBusy >= (numBlock / 2); // True, if half of block is busy - const maxPriorityFeePerGas = history.reward.reduce((previous, rewards) => { - let firstBN = BN_ZERO; - let firstIndex = 0; - - /* Get first priority which greater than 0 */ - for (let i = 0; i < rewards.length; i++) { - firstIndex = i; - const current = rewards[i]; - const currentBN = new BigN(current); - - if (currentBN.gt(BN_ZERO)) { - firstBN = currentBN; - - break; - } - } - - let secondBN = firstBN; - - /* Get second priority which greater than first priority */ - for (let i = firstIndex; i < rewards.length; i++) { - const current = rewards[i]; - const currentBN = new BigN(current); - - if (currentBN.gt(firstBN)) { - secondBN = currentBN; - - break; - } - } - - let current: BigN; - - if (busyNetwork) { - current = secondBN.dividedBy(2).gte(firstBN) ? firstBN : secondBN; // second too larger than first (> 2 times), use first else use second - } else { - current = firstBN; - } - - if (busyNetwork) { - /* Get max value */ - return current.gte(previous) ? current : previous; // get max priority - } else { - /* Get min value which greater than 0 */ - if (previous.eq(BN_ZERO)) { - return current; // get min priority - } else if (current.eq(BN_ZERO)) { - return previous; - } - - return current.lte(previous) ? current : previous; // get min priority - } - }, BN_ZERO); - - if (maxPriorityFeePerGas.eq(BN_ZERO)) { - const _price = await web3.api.eth.getGasPrice(); - const gasPrice = recalculateGasPrice(_price, networkKey); - - return { - gasPrice, - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, - baseGasFee: undefined, - busyNetwork: false - }; - } - - /* Max gas = (base + priority) * 1.5 (if not busy or 2 when busy); */ - const maxFeePerGas = baseGasFee.plus(maxPriorityFeePerGas).multipliedBy(busyNetwork ? 2 : 1.5).decimalPlaces(0); + const slowPriorityFee = history.reward.reduce((previous, rewards) => previous.plus(rewards[0]), BN_ZERO).dividedBy(numBlock).decimalPlaces(0); + const averagePriorityFee = history.reward.reduce((previous, rewards) => previous.plus(rewards[1]), BN_ZERO).dividedBy(numBlock).decimalPlaces(0); + const fastPriorityFee = history.reward.reduce((previous, rewards) => previous.plus(rewards[2]), BN_ZERO).dividedBy(numBlock).decimalPlaces(0); return { + type: 'evm', gasPrice: undefined, - maxFeePerGas, - maxPriorityFeePerGas, - baseGasFee, - busyNetwork + baseGasFee: baseGasFee.toString(), + busyNetwork, + options: { + slow: getEIP1559GasFee(baseGasFee, slowPriorityFee, 10, blockTime), + average: getEIP1559GasFee(baseGasFee, averagePriorityFee, 5, blockTime), + fast: getEIP1559GasFee(baseGasFee, fastPriorityFee, 3, blockTime), + default: busyNetwork ? 'average' : 'slow' + } }; } catch (e) { const _price = await web3.api.eth.getGasPrice(); const gasPrice = recalculateGasPrice(_price, networkKey); return { + type: 'evm', + busyNetwork: false, gasPrice, - maxFeePerGas: undefined, - maxPriorityFeePerGas: undefined, baseGasFee: undefined, - busyNetwork: false + options: undefined }; } }; + +export const calculateToAmountByReservePool = async (api: ApiPromise, fromToken: _ChainAsset, toToken: _ChainAsset, fromAmount: string): Promise => { + const reserve = await getReserveForPool(api, fromToken, toToken); + + return estimateTokensForPool(fromAmount, reserve); +}; diff --git a/packages/extension-base/src/services/storage-service/db-stores/Balance.ts b/packages/extension-base/src/services/storage-service/db-stores/Balance.ts index bae2c187530..86553422a28 100644 --- a/packages/extension-base/src/services/storage-service/db-stores/Balance.ts +++ b/packages/extension-base/src/services/storage-service/db-stores/Balance.ts @@ -11,6 +11,10 @@ export default class BalanceStore extends BaseStoreWithAddress { return this.table.where('address').anyOf(addresses).toArray(); } + async getBalanceHasAmount (address: string, chain: string): Promise { + return this.table.filter((item) => item.address === address && item.free !== '0' && item.tokenSlug.startsWith(chain)).toArray(); + } + async removeBySlugs (tokenSlugs: string[]) { return this.table.where('tokenSlug').anyOfIgnoreCase(tokenSlugs).delete(); } diff --git a/packages/extension-base/src/services/swap-service/handler/asset-hub/handler.ts b/packages/extension-base/src/services/swap-service/handler/asset-hub/handler.ts index 282e20d12d6..293fb61416b 100644 --- a/packages/extension-base/src/services/swap-service/handler/asset-hub/handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/asset-hub/handler.ts @@ -9,10 +9,12 @@ import { BalanceService } from '@subwallet/extension-base/services/balance-servi import { createXcmExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _getChainNativeTokenSlug, _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { convertSwapRate, getSwapAlternativeAsset, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils'; import { BasicTxErrorType, RequestCrossChainTransfer, RuntimeDispatchInfo } from '@subwallet/extension-base/types'; import { BaseStepDetail, CommonFeeComponent, CommonOptimalPath, CommonStepFeeInfo, CommonStepType } from '@subwallet/extension-base/types/service-base'; import { AssetHubSwapEarlyValidation, OptimalSwapPathParams, SwapBaseTxData, SwapErrorType, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; +import { getId } from '@subwallet/extension-base/utils/getId'; import BigN from 'bignumber.js'; import { SwapBaseHandler, SwapBaseInterface } from '../base-handler'; @@ -27,7 +29,7 @@ export class AssetHubSwapHandler implements SwapBaseInterface { isReady = false; providerSlug: SwapProviderId; - constructor (chainService: ChainService, balanceService: BalanceService, chain: string) { + constructor (chainService: ChainService, balanceService: BalanceService, feeService: FeeService, chain: string) { const chainInfo = chainService.getChainInfoByKey(chain); const providerSlug = chain === 'statemint' ? SwapProviderId.POLKADOT_ASSET_HUB @@ -39,7 +41,8 @@ export class AssetHubSwapHandler implements SwapBaseInterface { balanceService, chainService, providerName: chainInfo.name, - providerSlug + providerSlug, + feeService }); this.providerSlug = providerSlug; @@ -110,6 +113,7 @@ export class AssetHubSwapHandler implements SwapBaseInterface { try { const alternativeChainInfo = this.chainService.getChainInfoByKey(alternativeAsset.originChain); + const originalChainInfo = this.chainService.getChainInfoByKey(this.chain); const step: BaseStepDetail = { metadata: { sendingValue: bnAmount.toString(), @@ -121,14 +125,19 @@ export class AssetHubSwapHandler implements SwapBaseInterface { }; const xcmOriginSubstrateApi = await this.chainService.getSubstrateApi(alternativeAsset.originChain).isReady; + const id = getId(); + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(id, alternativeChainInfo.slug, 'substrate'); const xcmTransfer = await createXcmExtrinsic({ originTokenInfo: alternativeAsset, destinationTokenInfo: fromAsset, sendingValue: bnAmount.toString(), recipient: params.request.address, - chainInfoMap: this.chainService.getChainInfoMap(), - substrateApi: xcmOriginSubstrateApi + sender: params.request.address, + feeInfo: feeInfo, + substrateApi: xcmOriginSubstrateApi, + destinationChain: originalChainInfo, + originChain: alternativeChainInfo }); const _xcmFeeInfo = await xcmTransfer.paymentInfo(params.request.address); @@ -236,6 +245,9 @@ export class AssetHubSwapHandler implements SwapBaseInterface { const originAsset = this.chainService.getAssetBySlug(alternativeAssetSlug); const destinationAsset = this.chainService.getAssetBySlug(pair.from); + const originChain = this.chainService.getChainInfoByKey(originAsset.originChain); + const destinationChain = this.chainService.getChainInfoByKey(destinationAsset.originChain); + const substrateApi = this.chainService.getSubstrateApi(originAsset.originChain); const chainApi = await substrateApi.isReady; @@ -245,6 +257,8 @@ export class AssetHubSwapHandler implements SwapBaseInterface { const bnAmount = new BigN(params.quote.fromAmount); const bnDestinationAssetBalance = new BigN(destinationAssetBalance.value); + const id = getId(); + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(id, originChain.slug, 'substrate'); let bnTotalAmount = bnAmount.minus(bnDestinationAssetBalance); @@ -259,8 +273,11 @@ export class AssetHubSwapHandler implements SwapBaseInterface { destinationTokenInfo: destinationAsset, sendingValue: bnTotalAmount.toString(), recipient: params.address, - chainInfoMap: this.chainService.getChainInfoMap(), - substrateApi: chainApi + substrateApi: chainApi, + sender: params.address, + originChain: originChain, + destinationChain: destinationChain, + feeInfo }); const xcmData: RequestCrossChainTransfer = { diff --git a/packages/extension-base/src/services/swap-service/handler/asset-hub/router.ts b/packages/extension-base/src/services/swap-service/handler/asset-hub/router.ts index ff4f0cb6bee..b04a280d946 100644 --- a/packages/extension-base/src/services/swap-service/handler/asset-hub/router.ts +++ b/packages/extension-base/src/services/swap-service/handler/asset-hub/router.ts @@ -5,7 +5,7 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _getTokenMinAmount } from '@subwallet/extension-base/services/chain-service/utils'; -import { buildSwapExtrinsic, checkLiquidityForPath, checkMinAmountForPath, estimatePriceImpactPct, estimateRateAfter, estimateRateForPath, estimateTokensForPath, getReserveForPath } from '@subwallet/extension-base/services/swap-service/handler/asset-hub/utils'; +import { buildSwapExtrinsic, checkLiquidityForPath, checkMinAmountForPath, estimatePriceImpactPct, estimateRateAfterForPath, estimateRateForPath, estimateTokensForPath, getReserveForPath } from '@subwallet/extension-base/services/swap-service/handler/asset-hub/utils'; import { AssetHubPreValidationMetadata, AssetHubSwapEarlyValidation, SwapErrorType, SwapPair, SwapRequest } from '@subwallet/extension-base/types/swap'; import BigN from 'bignumber.js'; @@ -52,7 +52,7 @@ export class AssetHubRouter { const reserves = await getReserveForPath(api, paths); const amounts = estimateTokensForPath(amount, reserves); const marketRate = estimateRateForPath(reserves); - const marketRateAfter = estimateRateAfter(amount, reserves); + const marketRateAfter = estimateRateAfterForPath(amount, reserves); const priceImpactPct = estimatePriceImpactPct(marketRate, marketRateAfter); const errors: SwapErrorType[] = []; diff --git a/packages/extension-base/src/services/swap-service/handler/asset-hub/utils.ts b/packages/extension-base/src/services/swap-service/handler/asset-hub/utils.ts index 47531245090..22caa85e0d3 100644 --- a/packages/extension-base/src/services/swap-service/handler/asset-hub/utils.ts +++ b/packages/extension-base/src/services/swap-service/handler/asset-hub/utils.ts @@ -51,7 +51,7 @@ export const getReserveForPath = async (api: ApiPromise, paths: _ChainAsset[]): }; export const estimateTokensForPool = (amount: string, reserves: [string, string]): string => { - if (amount === '0') { + if (!amount || amount === '0') { return '0'; } @@ -97,7 +97,7 @@ export const estimateActualRate = (amount: string, reserves: Array<[string, stri return result.toString(); }; -export const estimateRateAfter = (amount: string, reserves: Array<[string, string]>): string => { +export const estimateRateAfterForPath = (amount: string, reserves: Array<[string, string]>): string => { const m = new BigN(amount); const reserve = reserves[0]; diff --git a/packages/extension-base/src/services/swap-service/handler/base-handler.ts b/packages/extension-base/src/services/swap-service/handler/base-handler.ts index 0e250e95a73..658fee10e05 100644 --- a/packages/extension-base/src/services/swap-service/handler/base-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/base-handler.ts @@ -7,6 +7,7 @@ import { _validateBalanceToSwap, _validateSwapRecipient } from '@subwallet/exten import { BalanceService } from '@subwallet/extension-base/services/balance-service'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { getSwapAlternativeAsset } from '@subwallet/extension-base/services/swap-service/utils'; import { BasicTxErrorType } from '@subwallet/extension-base/types'; import { BaseStepDetail, CommonOptimalPath, CommonStepFeeInfo, DEFAULT_FIRST_STEP, MOCK_STEP_FEE } from '@subwallet/extension-base/types/service-base'; @@ -37,7 +38,8 @@ export interface SwapBaseHandlerInitParams { providerSlug: SwapProviderId, providerName: string, chainService: ChainService, - balanceService: BalanceService + balanceService: BalanceService, + feeService: FeeService; } export class SwapBaseHandler { @@ -45,12 +47,14 @@ export class SwapBaseHandler { private readonly providerName: string; public chainService: ChainService; public balanceService: BalanceService; + public feeService: FeeService; - public constructor ({ balanceService, chainService, providerName, providerSlug }: SwapBaseHandlerInitParams) { + public constructor ({ balanceService, chainService, feeService, providerName, providerSlug }: SwapBaseHandlerInitParams) { this.providerName = providerName; this.providerSlug = providerSlug; this.chainService = chainService; this.balanceService = balanceService; + this.feeService = feeService; } // public abstract getSwapQuote(request: SwapRequest): Promise; diff --git a/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts b/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts index fc9d21f1a03..949cc619a29 100644 --- a/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/chainflip-handler.ts @@ -13,11 +13,13 @@ import { getERC20TransactionObject, getEVMTransactionObject } from '@subwallet/e import { createTransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenSlug, _getContractAddressOfToken, _isChainSubstrateCompatible, _isNativeToken, _isSmartContractToken } from '@subwallet/extension-base/services/chain-service/utils'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { SwapBaseHandler, SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler'; import { calculateSwapRate, CHAIN_FLIP_SUPPORTED_MAINNET_ASSET_MAPPING, CHAIN_FLIP_SUPPORTED_MAINNET_MAPPING, CHAIN_FLIP_SUPPORTED_TESTNET_ASSET_MAPPING, CHAIN_FLIP_SUPPORTED_TESTNET_MAPPING, getChainflipOptions, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils'; import { BasicTxErrorType, TransactionData } from '@subwallet/extension-base/types'; import { BaseStepDetail, CommonFeeComponent, CommonOptimalPath, CommonStepFeeInfo, CommonStepType } from '@subwallet/extension-base/types/service-base'; import { ChainflipPreValidationMetadata, ChainflipSwapTxData, OptimalSwapPathParams, SwapEarlyValidation, SwapErrorType, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { AxiosError } from 'axios'; import BigNumber from 'bignumber.js'; @@ -48,10 +50,11 @@ export class ChainflipSwapHandler implements SwapBaseInterface { private swapBaseHandler: SwapBaseHandler; providerSlug: SwapProviderId; - constructor (chainService: ChainService, balanceService: BalanceService, isTestnet = true) { + constructor (chainService: ChainService, balanceService: BalanceService, feeService: FeeService, isTestnet = true) { this.swapBaseHandler = new SwapBaseHandler({ chainService, balanceService, + feeService, providerName: isTestnet ? 'Chainflip Testnet' : 'Chainflip', providerSlug: isTestnet ? SwapProviderId.CHAIN_FLIP_TESTNET : SwapProviderId.CHAIN_FLIP_MAINNET }); @@ -442,12 +445,32 @@ export class ChainflipSwapHandler implements SwapBaseInterface { extrinsic = submittableExtrinsic as SubmittableExtrinsic<'promise'>; } else { + const id = getId(); + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(id, chainInfo.slug, 'evm'); + if (_isNativeToken(fromAsset)) { - const [transactionConfig] = await getEVMTransactionObject(chainInfo, address, depositAddressResponse.depositAddress, quote.fromAmount, false, this.chainService.getEvmApi(chainInfo.slug)); + const [transactionConfig] = await getEVMTransactionObject({ + chain: chainInfo.slug, + evmApi: this.chainService.getEvmApi(chainInfo.slug), + from: address, + to: depositAddressResponse.depositAddress, + value: quote.fromAmount, + feeInfo, + transferAll: false + }); extrinsic = transactionConfig; } else { - const [transactionConfig] = await getERC20TransactionObject(_getContractAddressOfToken(fromAsset), chainInfo, address, depositAddressResponse.depositAddress, quote.fromAmount, false, this.chainService.getEvmApi(chainInfo.slug)); + const [transactionConfig] = await getERC20TransactionObject({ + assetAddress: _getContractAddressOfToken(fromAsset), + chain: chainInfo.slug, + evmApi: this.chainService.getEvmApi(chainInfo.slug), + from: address, + to: depositAddressResponse.depositAddress, + value: quote.fromAmount, + feeInfo, + transferAll: false + }); extrinsic = transactionConfig; } diff --git a/packages/extension-base/src/services/swap-service/handler/hydradx-handler.ts b/packages/extension-base/src/services/swap-service/handler/hydradx-handler.ts index dbf56275e15..de41a141cae 100644 --- a/packages/extension-base/src/services/swap-service/handler/hydradx-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/hydradx-handler.ts @@ -12,11 +12,13 @@ import { BalanceService } from '@subwallet/extension-base/services/balance-servi import { createXcmExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; import { _getAssetDecimals, _getChainNativeTokenSlug, _getTokenOnChainAssetId, _isNativeToken } from '@subwallet/extension-base/services/chain-service/utils'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { SwapBaseHandler, SwapBaseInterface } from '@subwallet/extension-base/services/swap-service/handler/base-handler'; import { calculateSwapRate, getSwapAlternativeAsset, SWAP_QUOTE_TIMEOUT_MAP } from '@subwallet/extension-base/services/swap-service/utils'; import { BasicTxErrorType, RequestCrossChainTransfer, RuntimeDispatchInfo } from '@subwallet/extension-base/types'; import { BaseStepDetail, CommonFeeComponent, CommonOptimalPath, CommonStepFeeInfo, CommonStepType } from '@subwallet/extension-base/types/service-base'; import { HydradxPreValidationMetadata, HydradxSwapTxData, OptimalSwapPathParams, SwapEarlyValidation, SwapErrorType, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapRoute, SwapStepType, SwapSubmitParams, SwapSubmitStepData, ValidateSwapProcessParams } from '@subwallet/extension-base/types/swap'; +import { getId } from '@subwallet/extension-base/utils/getId'; import BigNumber from 'bignumber.js'; import { SubmittableExtrinsic } from '@polkadot/api/types'; @@ -36,10 +38,11 @@ export class HydradxHandler implements SwapBaseInterface { public isReady = false; providerSlug: SwapProviderId; - constructor (chainService: ChainService, balanceService: BalanceService, isTestnet = true) { + constructor (chainService: ChainService, balanceService: BalanceService, feeService: FeeService, isTestnet = true) { this.swapBaseHandler = new SwapBaseHandler({ balanceService, chainService, + feeService, providerName: isTestnet ? 'Hydration Testnet' : 'Hydration', providerSlug: isTestnet ? SwapProviderId.HYDRADX_TESTNET : SwapProviderId.HYDRADX_MAINNET }); @@ -121,6 +124,7 @@ export class HydradxHandler implements SwapBaseInterface { try { const alternativeChainInfo = this.chainService.getChainInfoByKey(alternativeAsset.originChain); + const destChainInfo = this.chainService.getChainInfoByKey(this.chain()); const step: BaseStepDetail = { metadata: { sendingValue: bnAmount.toString(), @@ -132,14 +136,19 @@ export class HydradxHandler implements SwapBaseInterface { }; const xcmOriginSubstrateApi = await this.chainService.getSubstrateApi(alternativeAsset.originChain).isReady; + const id = getId(); + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(id, alternativeAsset.originChain, 'substrate'); const xcmTransfer = await createXcmExtrinsic({ originTokenInfo: alternativeAsset, destinationTokenInfo: fromAsset, sendingValue: bnAmount.toString(), recipient: params.request.address, - chainInfoMap: this.chainService.getChainInfoMap(), - substrateApi: xcmOriginSubstrateApi + substrateApi: xcmOriginSubstrateApi, + sender: params.request.address, + destinationChain: destChainInfo, + originChain: alternativeChainInfo, + feeInfo }); const _xcmFeeInfo = await xcmTransfer.paymentInfo(params.request.address); @@ -380,6 +389,9 @@ export class HydradxHandler implements SwapBaseInterface { const originAsset = this.chainService.getAssetBySlug(alternativeAssetSlug); const destinationAsset = this.chainService.getAssetBySlug(pair.from); + const originChain = this.chainService.getChainInfoByKey(originAsset.originChain); + const destinationChain = this.chainService.getChainInfoByKey(destinationAsset.originChain); + const substrateApi = this.chainService.getSubstrateApi(originAsset.originChain); const chainApi = await substrateApi.isReady; @@ -398,13 +410,18 @@ export class HydradxHandler implements SwapBaseInterface { bnTotalAmount = bnTotalAmount.plus(bnXcmFee); } + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(getId(), originAsset.originChain, 'substrate'); + const xcmTransfer = await createXcmExtrinsic({ originTokenInfo: originAsset, destinationTokenInfo: destinationAsset, sendingValue: bnTotalAmount.toString(), recipient: params.address, - chainInfoMap: this.chainService.getChainInfoMap(), - substrateApi: chainApi + substrateApi: chainApi, + sender: params.address, + destinationChain, + originChain, + feeInfo }); const xcmData: RequestCrossChainTransfer = { diff --git a/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts index 58c8ba2180f..269401782f5 100644 --- a/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts +++ b/packages/extension-base/src/services/swap-service/handler/simpleswap-handler.ts @@ -7,8 +7,10 @@ import { TransactionError } from '@subwallet/extension-base/background/errors/Tr import { ChainType, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { _getSimpleSwapEarlyValidationError } from '@subwallet/extension-base/core/logic-validation/swap'; import { _getAssetDecimals, _getChainNativeTokenSlug, _getContractAddressOfToken, _isChainSubstrateCompatible, _isNativeToken, _isSmartContractToken } from '@subwallet/extension-base/services/chain-service/utils'; +import FeeService from '@subwallet/extension-base/services/fee-service/service'; import { BaseStepDetail, BasicTxErrorType, CommonFeeComponent, CommonOptimalPath, CommonStepFeeInfo, CommonStepType, OptimalSwapPathParams, SimpleSwapTxData, SimpleSwapValidationMetadata, SwapEarlyValidation, SwapErrorType, SwapFeeType, SwapProviderId, SwapQuote, SwapRequest, SwapStepType, SwapSubmitParams, SwapSubmitStepData, TransactionData, ValidateSwapProcessParams } from '@subwallet/extension-base/types'; import { _reformatAddressWithChain, formatNumber } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import BigN, { BigNumber } from 'bignumber.js'; import { SubmittableExtrinsic } from '@polkadot/api/types'; @@ -159,10 +161,11 @@ export class SimpleSwapHandler implements SwapBaseInterface { private swapBaseHandler: SwapBaseHandler; providerSlug: SwapProviderId; - constructor (chainService: ChainService, balanceService: BalanceService) { + constructor (chainService: ChainService, balanceService: BalanceService, feeService: FeeService) { this.swapBaseHandler = new SwapBaseHandler({ chainService, balanceService, + feeService, providerName: 'SimpleSwap', providerSlug: SwapProviderId.SIMPLE_SWAP }); @@ -476,27 +479,31 @@ export class SimpleSwapHandler implements SwapBaseInterface { extrinsic = submittableExtrinsic as SubmittableExtrinsic<'promise'>; } else { + const feeInfo = await this.swapBaseHandler.feeService.subscribeChainFee(getId(), chainInfo.slug, 'evm'); + if (_isNativeToken(fromAsset)) { - const [transactionConfig] = await getEVMTransactionObject( - chainInfo, - address, - addressFrom, - quote.fromAmount, - false, - this.chainService.getEvmApi(chainInfo.slug) - ); + const [transactionConfig] = await getEVMTransactionObject({ + evmApi: this.chainService.getEvmApi(chainInfo.slug), + transferAll: false, + value: quote.fromAmount, + from: address, + to: addressFrom, + chain: chainInfo.slug, + feeInfo + }); extrinsic = transactionConfig; } else { - const [transactionConfig] = await getERC20TransactionObject( - _getContractAddressOfToken(fromAsset), - chainInfo, - address, - addressFrom, - quote.fromAmount, - false, - this.chainService.getEvmApi(chainInfo.slug) - ); + const [transactionConfig] = await getERC20TransactionObject({ + assetAddress: _getContractAddressOfToken(fromAsset), + chain: chainInfo.slug, + evmApi: this.chainService.getEvmApi(chainInfo.slug), + feeInfo, + from: address, + to: addressFrom, + value: quote.fromAmount, + transferAll: false + }); extrinsic = transactionConfig; } diff --git a/packages/extension-base/src/services/swap-service/index.ts b/packages/extension-base/src/services/swap-service/index.ts index 9a2231d94ae..ec01da92058 100644 --- a/packages/extension-base/src/services/swap-service/index.ts +++ b/packages/extension-base/src/services/swap-service/index.ts @@ -165,33 +165,33 @@ export class SwapService implements ServiceWithProcessInterface, StoppableServic _SUPPORTED_SWAP_PROVIDERS.forEach((providerId) => { switch (providerId) { case SwapProviderId.CHAIN_FLIP_TESTNET: - this.handlers[providerId] = new ChainflipSwapHandler(this.chainService, this.state.balanceService); + this.handlers[providerId] = new ChainflipSwapHandler(this.chainService, this.state.balanceService, this.state.feeService); break; case SwapProviderId.CHAIN_FLIP_MAINNET: - this.handlers[providerId] = new ChainflipSwapHandler(this.chainService, this.state.balanceService, false); + this.handlers[providerId] = new ChainflipSwapHandler(this.chainService, this.state.balanceService, this.state.feeService, false); break; case SwapProviderId.HYDRADX_TESTNET: - this.handlers[providerId] = new HydradxHandler(this.chainService, this.state.balanceService); + this.handlers[providerId] = new HydradxHandler(this.chainService, this.state.balanceService, this.state.feeService); break; case SwapProviderId.HYDRADX_MAINNET: - this.handlers[providerId] = new HydradxHandler(this.chainService, this.state.balanceService, false); + this.handlers[providerId] = new HydradxHandler(this.chainService, this.state.balanceService, this.state.feeService, false); break; case SwapProviderId.POLKADOT_ASSET_HUB: - this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, 'statemint'); + this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, this.state.feeService, 'statemint'); break; case SwapProviderId.KUSAMA_ASSET_HUB: - this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, 'statemine'); + this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, this.state.feeService, 'statemine'); break; case SwapProviderId.ROCOCO_ASSET_HUB: - this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, 'rococo_assethub'); + this.handlers[providerId] = new AssetHubSwapHandler(this.chainService, this.state.balanceService, this.state.feeService, 'rococo_assethub'); break; case SwapProviderId.SIMPLE_SWAP: - this.handlers[providerId] = new SimpleSwapHandler(this.chainService, this.state.balanceService); + this.handlers[providerId] = new SimpleSwapHandler(this.chainService, this.state.balanceService, this.state.feeService); break; default: diff --git a/packages/extension-base/src/services/transaction-service/balance.spec.ts b/packages/extension-base/src/services/transaction-service/balance.spec.ts index a3d229b4daa..634d919a142 100644 --- a/packages/extension-base/src/services/transaction-service/balance.spec.ts +++ b/packages/extension-base/src/services/transaction-service/balance.spec.ts @@ -8,6 +8,7 @@ import { createTransferExtrinsic } from '@subwallet/extension-base/services/bala import { EvmChainHandler } from '@subwallet/extension-base/services/chain-service/handler/EvmChainHandler'; import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain-service/handler/SubstrateChainHandler'; import { _getContractAddressOfToken, _isLocalToken, _isTokenEvmSmartContract } from '@subwallet/extension-base/services/chain-service/utils'; +import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; import BigN from 'bignumber.js'; import fs from 'fs'; import { TransactionConfig } from 'web3-core'; @@ -102,11 +103,29 @@ describe('test token transfer', () => { for (const asset of assets) { try { let transaction: TransactionConfig; + const feeInfo = await calculateGasFeeParams(_api, chain.slug); if (_isTokenEvmSmartContract(asset) || _isLocalToken(asset)) { - [transaction] = await getERC20TransactionObject(_getContractAddressOfToken(asset), chain, '0x29d6d6d84c9662486198667b5a9fbda3e698b23f', '0x5e10e440FEce4dB0b16a6159A4536efb74d32E9b', '0', false, _api); + [transaction] = await getERC20TransactionObject({ + assetAddress: _getContractAddressOfToken(asset), + chain: chain.slug, + evmApi: _api, + feeInfo, + value: '0', + from: '0x29d6d6d84c9662486198667b5a9fbda3e698b23f', + to: '0x5e10e440FEce4dB0b16a6159A4536efb74d32E9b', + transferAll: false + }); } else { - [transaction] = await getEVMTransactionObject(chain, '0x29d6d6d84c9662486198667b5a9fbda3e698b23f', '0x5e10e440FEce4dB0b16a6159A4536efb74d32E9b', '0', false, _api); + [transaction] = await getEVMTransactionObject({ + chain: chain.slug, + evmApi: _api, + feeInfo, + from: '0x29d6d6d84c9662486198667b5a9fbda3e698b23f', + to: '0x5e10e440FEce4dB0b16a6159A4536efb74d32E9b', + value: '0', + transferAll: false + }); } if (transaction) { diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 2249f181183..9c26aa6f024 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -22,13 +22,15 @@ import { getBaseTransactionInfo, getTransactionId, isSubstrateTransaction, isTon import { SWTransaction, SWTransactionInput, SWTransactionResponse, TransactionEmitter, TransactionEventMap, TransactionEventResponse, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { getExplorerLink, parseTransactionData } from '@subwallet/extension-base/services/transaction-service/utils'; import { isWalletConnectRequest } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; -import { AccountJson, BasicTxErrorType, BasicTxWarningCode, LeavePoolAdditionalData, RequestStakePoolingBonding, RequestYieldStepSubmit, SpecialYieldPoolInfo, SubmitJoinNominationPool, Web3Transaction, YieldPoolType } from '@subwallet/extension-base/types'; +import { AccountJson, BasicTxErrorType, BasicTxWarningCode, EvmFeeInfo, LeavePoolAdditionalData, RequestStakePoolingBonding, RequestYieldStepSubmit, SpecialYieldPoolInfo, SubmitJoinNominationPool, SubstrateTipInfo, Web3Transaction, YieldPoolType } from '@subwallet/extension-base/types'; import { _isRuntimeUpdated, anyNumberToBN, pairToAccount, reformatAddress } from '@subwallet/extension-base/utils'; import { mergeTransactionAndSignature } from '@subwallet/extension-base/utils/eth/mergeTransactionAndSignature'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { BN_ZERO } from '@subwallet/extension-base/utils/number'; import keyring from '@subwallet/ui-keyring'; import { Cell } from '@ton/core'; +import BigN from 'bignumber.js'; import { addHexPrefix } from 'ethereumjs-util'; import { ethers, TransactionLike } from 'ethers'; import EventEmitter from 'eventemitter3'; @@ -117,6 +119,8 @@ export default class TransactionService { } const transaction = transactionInput.transaction; + const nativeTokenInfo = this.state.chainService.getNativeTokenInfo(chain); + const tokenPayFeeInfo = transactionInput.nonNativeTokenPayFeeSlug ? this.chainService.getAssetBySlug(transactionInput.nonNativeTokenPayFeeSlug) : undefined; // Check duplicated transaction validationResponse.errors.push(...this.checkDuplicate(transactionInput)); @@ -128,6 +132,7 @@ export default class TransactionService { validationResponse.errors.push(new TransactionError(BasicTxErrorType.INTERNAL_ERROR, t('Cannot find network'))); } + const substrateApi = this.state.chainService.getSubstrateApi(chainInfo.slug); const evmApi = this.state.chainService.getEvmApi(chainInfo.slug); const tonApi = this.state.chainService.getTonApi(chainInfo.slug); const isNoEvmApi = transaction && !isSubstrateTransaction(transaction) && !isTonTransaction(transaction) && !evmApi; // todo: should split isEvmTx && isNoEvmApi. Because other chains type also has no Evm Api @@ -138,14 +143,16 @@ export default class TransactionService { } // Estimate fee for transaction - validationResponse.estimateFee = await estimateFeeForTransaction(validationResponse, transaction, chainInfo, evmApi); + const id = getId(); + const feeInfo = await this.state.feeService.subscribeChainFee(id, chain, 'evm') as EvmFeeInfo; + + validationResponse.estimateFee = await estimateFeeForTransaction(validationResponse, transaction, chainInfo, evmApi, substrateApi, feeInfo, nativeTokenInfo, tokenPayFeeInfo, transactionInput.isTransferLocalTokenAndPayThatTokenAsFee); const chainInfoMap = this.state.chainService.getChainInfoMap(); // Check account signing transaction checkSigningAccountForTransaction(validationResponse, chainInfoMap); - const nativeTokenInfo = this.state.chainService.getNativeTokenInfo(chain); const nativeTokenAvailable = await this.state.balanceService.getTransferableBalance(address, chain, nativeTokenInfo.slug, extrinsicType); // Check available balance against transaction fee @@ -924,6 +931,14 @@ export default class TransactionService { payload.from = address; } + if (!payload.estimateGas) { + if (payload.maxFeePerGas) { + payload.estimateGas = new BigN(anyNumberToBN(payload.maxFeePerGas).toNumber()).multipliedBy(payload.gas || '0').toFixed(0); + } else { + payload.estimateGas = new BigN(anyNumberToBN(payload.gasPrice).toNumber()).multipliedBy(payload.gas || '0').toFixed(0); + } + } + const isExternal = !!account.isExternal; const isInjected = !!account.isInjected; @@ -1098,7 +1113,10 @@ export default class TransactionService { return emitter; } - private signAndSendSubstrateTransaction ({ address, chain, id, transaction, url }: SWTransaction): TransactionEmitter { + private signAndSendSubstrateTransaction ({ address, chain, feeCustom, id, nonNativeTokenPayFeeSlug, transaction, url }: SWTransaction): TransactionEmitter { + const tip = (feeCustom as SubstrateTipInfo)?.tip || '0'; + const feeAssetId = nonNativeTokenPayFeeSlug ? this.state.chainService.getAssetBySlug(nonNativeTokenPayFeeSlug).metadata?.multilocation as Record : undefined; + const emitter = new EventEmitter(); const eventData: TransactionEventResponse = { id, @@ -1123,7 +1141,9 @@ export default class TransactionService { } as SignerResult; } } as Signer, - withSignedTransaction: true + tip, + withSignedTransaction: true, + assetId: feeAssetId }; // if (_isRuntimeUpdated(signedExtensions)) { diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index de409489cff..7d0f0385ba7 100644 --- a/packages/extension-base/src/services/transaction-service/types.ts +++ b/packages/extension-base/src/services/transaction-service/types.ts @@ -3,14 +3,14 @@ import { ChainType, ExtrinsicDataTypeMap, ExtrinsicStatus, ExtrinsicType, FeeData, ValidateTransactionResponse } from '@subwallet/extension-base/background/KoniTypes'; import { TonTransactionConfig } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; -import { BaseRequestSign } from '@subwallet/extension-base/types'; +import { BaseRequestSign, TransactionFee } from '@subwallet/extension-base/types'; import EventEmitter from 'eventemitter3'; import { TransactionConfig } from 'web3-core'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import { EventRecord } from '@polkadot/types/interfaces'; -export interface SWTransaction extends ValidateTransactionResponse, Partial> { +export interface SWTransaction extends ValidateTransactionResponse, Partial>, TransactionFee { id: string; url?: string; isInternal: boolean, @@ -34,18 +34,19 @@ export type SWTransactionResult = Omit & Partial>; -export interface SWTransactionInput extends SwInputBase, Partial> { +export interface SWTransactionInput extends SwInputBase, Partial>, TransactionFee { id?: string; transaction?: SWTransaction['transaction'] | null; warnings?: SWTransaction['warnings']; errors?: SWTransaction['errors']; edAsWarning?: boolean; isTransferAll?: boolean; + isTransferLocalTokenAndPayThatTokenAsFee?: boolean; resolveOnDone?: boolean; skipFeeValidation?: boolean; } -export type SWTransactionResponse = SwInputBase & Pick & Partial>; +export type SWTransactionResponse = SwInputBase & Pick & Partial> & TransactionFee; export type ValidateTransactionResponseInput = SWTransactionInput; diff --git a/packages/extension-base/src/services/transaction-service/xcm.spec.ts b/packages/extension-base/src/services/transaction-service/xcm.spec.ts index 79132148b3f..eb7e4ef80d7 100644 --- a/packages/extension-base/src/services/transaction-service/xcm.spec.ts +++ b/packages/extension-base/src/services/transaction-service/xcm.spec.ts @@ -7,6 +7,7 @@ import { createXcmExtrinsic } from '@subwallet/extension-base/services/balance-s import { SubstrateChainHandler } from '@subwallet/extension-base/services/chain-service/handler/SubstrateChainHandler'; import { _SubstrateApi } from '@subwallet/extension-base/services/chain-service/types'; import { _isChainEvmCompatible } from '@subwallet/extension-base/services/chain-service/utils'; +import { SubstrateFeeInfo } from '@subwallet/extension-base/types'; import { cryptoWaitReady } from '@polkadot/util-crypto'; @@ -56,9 +57,26 @@ describe('test token transfer', () => { const originTokenInfo = ChainAssetMap[assetRef.srcAsset]; const destinationTokenInfo = ChainAssetMap[assetRef.destAsset]; + const originChain = ChainInfoMap[assetRef.srcChain]; const destChain = ChainInfoMap[assetRef.destChain]; const isDestChainEvm = _isChainEvmCompatible(destChain); const destAddress = isDestChainEvm ? destAddress2 : destAddress1; + const feeInfo: SubstrateFeeInfo = { + type: 'substrate', + options: { + slow: { + tip: '0' + }, + fast: { + tip: '0' + }, + average: { + tip: '0' + }, + default: 'slow' + }, + busyNetwork: false + }; try { await createXcmExtrinsic({ @@ -66,8 +84,11 @@ describe('test token transfer', () => { originTokenInfo, sendingValue: '0', recipient: destAddress, - chainInfoMap: ChainInfoMap, - substrateApi + substrateApi, + sender: '5DnokDpMdNEH8cApsZoWQnjsggADXQmGWUb6q8ZhHeEwvncL', + feeInfo, + originChain, + destinationChain: destChain }); } catch (e) { console.log(e); @@ -103,8 +124,26 @@ describe('test token transfer', () => { const substrateApi = await substrateApiMap[assetRef.srcChain].isReady; const destinationTokenInfo = ChainAssetMap[assetRef.destAsset]; const originTokenInfo = ChainAssetMap[assetRef.srcAsset]; - const isDestChainEvm = _isChainEvmCompatible(ChainInfoMap[assetRef.destChain]); + const destinationChainInfo = ChainInfoMap[assetRef.destChain]; + const originChainInfo = ChainInfoMap[assetRef.srcChain]; + const isDestChainEvm = _isChainEvmCompatible(destinationChainInfo); const destAddress = isDestChainEvm ? destAddress2 : destAddress1; + const feeInfo: SubstrateFeeInfo = { + type: 'substrate', + options: { + slow: { + tip: '0' + }, + fast: { + tip: '0' + }, + average: { + tip: '0' + }, + default: 'slow' + }, + busyNetwork: false + }; try { const extrinsic = await createXcmExtrinsic({ @@ -112,8 +151,11 @@ describe('test token transfer', () => { originTokenInfo, sendingValue: '0', recipient: destAddress, - chainInfoMap: ChainInfoMap, - substrateApi + substrateApi, + sender: '5DnokDpMdNEH8cApsZoWQnjsggADXQmGWUb6q8ZhHeEwvncL', + feeInfo, + originChain: originChainInfo, + destinationChain: destinationChainInfo }); console.log(assetRef, extrinsic.toHex()); diff --git a/packages/extension-base/src/types/balance/transfer.ts b/packages/extension-base/src/types/balance/transfer.ts new file mode 100644 index 00000000000..3d115dd8f26 --- /dev/null +++ b/packages/extension-base/src/types/balance/transfer.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2022 @subwallet/extension-base authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { BaseRequestSign } from '@subwallet/extension-base/types'; + +import { FeeChainType, FeeDetail, TransactionFee } from '../fee'; + +export interface RequestSubscribeTransfer extends TransactionFee { + address: string; + chain: string; + token: string; + destChain: string; +} + +export interface ResponseSubscribeTransfer { + id: string; + maxTransferable: string; + feeOptions: FeeDetail; + feeType: FeeChainType; +} + +export interface RequestSubmitTransfer extends BaseRequestSign, TransactionFee { + chain: string; + from: string; + to: string; + tokenSlug: string; + transferAll: boolean; + value: string; + transferBounceable?: boolean; + isTransferLocalTokenAndPayThatTokenAsFee?: boolean; +} diff --git a/packages/extension-base/src/types/fee/base.ts b/packages/extension-base/src/types/fee/base.ts new file mode 100644 index 00000000000..05028b84060 --- /dev/null +++ b/packages/extension-base/src/types/fee/base.ts @@ -0,0 +1,13 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export type FeeChainType = 'evm' | 'substrate' | 'ton'; + +export interface BaseFeeInfo { + busyNetwork: boolean; + type: FeeChainType; +} + +export interface BaseFeeDetail { + estimatedFee: string; +} diff --git a/packages/extension-base/src/types/fee/evm.ts b/packages/extension-base/src/types/fee/evm.ts index 0de787635c9..88c624bae7b 100644 --- a/packages/extension-base/src/types/fee/evm.ts +++ b/packages/extension-base/src/types/fee/evm.ts @@ -1,44 +1,75 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 -import BigN from 'bignumber.js'; - -interface BaseFeeInfo { - // blockNumber: string; - busyNetwork: boolean; -} +import { BaseFeeDetail, BaseFeeInfo, FeeDefaultOption } from '@subwallet/extension-base/types'; export interface EvmLegacyFeeInfo extends BaseFeeInfo { + type: 'evm'; gasPrice: string; - maxFeePerGas: undefined; - maxPriorityFeePerGas: undefined; baseGasFee: undefined; + options: undefined; +} + +export interface EvmEIP1559FeeOption { + maxFeePerGas: string; + maxPriorityFeePerGas: string; + minWaitTimeEstimate?: number; + maxWaitTimeEstimate?: number; +} + +export enum FeeOptionKey { + SLOW = 'slow', + AVERAGE = 'average', + FAST = 'fast', + DEFAULT = 'default', } -export interface EvmEIP1995FeeInfo extends BaseFeeInfo { +export interface EvmEIP1559FeeInfo extends BaseFeeInfo { + type: 'evm'; gasPrice: undefined; - maxFeePerGas: BigN; - maxPriorityFeePerGas: BigN; - baseGasFee: BigN; + baseGasFee: string; + options: { + [FeeOptionKey.SLOW]: EvmEIP1559FeeOption; + [FeeOptionKey.AVERAGE]: EvmEIP1559FeeOption; + [FeeOptionKey.FAST]: EvmEIP1559FeeOption; + [FeeOptionKey.DEFAULT]: FeeDefaultOption; + } } -export type EvmFeeInfo = EvmLegacyFeeInfo | EvmEIP1995FeeInfo; +export type EvmFeeInfo = EvmLegacyFeeInfo | EvmEIP1559FeeInfo; export interface EvmLegacyFeeInfoCache extends BaseFeeInfo { + type: 'evm'; gasPrice: string; maxFeePerGas: undefined; maxPriorityFeePerGas: undefined; baseGasFee: undefined; + options: undefined; } -export interface EvmEIP1995FeeInfoCache extends BaseFeeInfo { +export interface EvmEIP1559FeeInfoCache extends BaseFeeInfo { + type: 'evm'; gasPrice: undefined; - maxFeePerGas: string; - maxPriorityFeePerGas: string; baseGasFee: string; + options: { + [FeeOptionKey.SLOW]: EvmEIP1559FeeOption; + [FeeOptionKey.AVERAGE]: EvmEIP1559FeeOption; + [FeeOptionKey.FAST]: EvmEIP1559FeeOption; + [FeeOptionKey.DEFAULT]: FeeDefaultOption; + } +} + +export interface EvmLegacyFeeDetail extends EvmLegacyFeeInfo, BaseFeeDetail { + gasLimit: string; } -export type EvmFeeInfoCache = EvmLegacyFeeInfoCache | EvmEIP1995FeeInfoCache; +export interface EvmEIP1559FeeDetail extends EvmEIP1559FeeInfo, BaseFeeDetail { + gasLimit: string; +} + +export type EvmFeeInfoCache = EvmLegacyFeeInfoCache | EvmEIP1559FeeInfoCache; + +export type EvmFeeDetail = EvmLegacyFeeDetail | EvmEIP1559FeeDetail; export interface InfuraFeeDetail { suggestedMaxPriorityFeePerGas: string; @@ -59,3 +90,7 @@ export interface InfuraFeeInfo { priorityFeeTrend: 'down' | 'up'; baseFeeTrend: 'down' | 'up'; } + +export interface InfuraThresholdInfo { + busyThreshold: string; // in gwei +} diff --git a/packages/extension-base/src/types/fee/index.ts b/packages/extension-base/src/types/fee/index.ts index 0bf28545582..0de282e4daf 100644 --- a/packages/extension-base/src/types/fee/index.ts +++ b/packages/extension-base/src/types/fee/index.ts @@ -1,5 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +export * from './base'; export * from './evm'; -export * from './fee'; +export * from './option'; +export * from './subscription'; +export * from './substrate'; diff --git a/packages/extension-base/src/types/fee/option.ts b/packages/extension-base/src/types/fee/option.ts new file mode 100644 index 00000000000..fa50157a8c7 --- /dev/null +++ b/packages/extension-base/src/types/fee/option.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { FeeCustom } from '@subwallet/extension-base/types'; + +export type FeeDefaultOption = 'slow' | 'average' | 'fast'; +export type FeeOption = FeeDefaultOption | 'custom'; + +export type TransactionFee = { + feeOption?: FeeOption; + feeCustom?: FeeCustom; + nonNativeTokenPayFeeSlug?: string; + isTransferLocalTokenAndPayThatTokenAsFee?: boolean; +} diff --git a/packages/extension-base/src/types/fee/subscription.ts b/packages/extension-base/src/types/fee/subscription.ts new file mode 100644 index 00000000000..8a8d34e628a --- /dev/null +++ b/packages/extension-base/src/types/fee/subscription.ts @@ -0,0 +1,18 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BehaviorSubject } from 'rxjs'; + +import { EvmEIP1559FeeOption, EvmFeeDetail, EvmFeeInfo } from './evm'; +import { SubstrateFeeDetail, SubstrateFeeInfo, SubstrateTipInfo } from './substrate'; +import { TonFeeDetail, TonFeeInfo, TonTipInfo } from './ton'; + +export type FeeInfo = EvmFeeInfo | SubstrateFeeInfo | TonFeeInfo; +export type FeeDetail = EvmFeeDetail | SubstrateFeeDetail | TonFeeDetail; +export type FeeCustom = EvmEIP1559FeeOption | SubstrateTipInfo | TonTipInfo; + +export interface FeeSubscription { + observer: BehaviorSubject; + subscription: Record; + unsubscribe: VoidFunction; +} diff --git a/packages/extension-base/src/types/fee/substrate.ts b/packages/extension-base/src/types/fee/substrate.ts new file mode 100644 index 00000000000..e84b037aaef --- /dev/null +++ b/packages/extension-base/src/types/fee/substrate.ts @@ -0,0 +1,21 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BaseFeeDetail, BaseFeeInfo } from './base'; +import { FeeDefaultOption } from './option'; + +export interface SubstrateTipInfo { + tip: string; +} + +export interface SubstrateFeeInfo extends BaseFeeInfo { + type: 'substrate'; + options: { + slow: SubstrateTipInfo; + average: SubstrateTipInfo; + fast: SubstrateTipInfo; + default: FeeDefaultOption; + } +} + +export type SubstrateFeeDetail = SubstrateFeeInfo & BaseFeeDetail; diff --git a/packages/extension-base/src/types/fee/ton.ts b/packages/extension-base/src/types/fee/ton.ts new file mode 100644 index 00000000000..2ee6d412670 --- /dev/null +++ b/packages/extension-base/src/types/fee/ton.ts @@ -0,0 +1,24 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { BaseFeeDetail, BaseFeeInfo } from './base'; +import { FeeDefaultOption } from './option'; + +/** @deprecated */ +export interface TonTipInfo { + tip: string; +} + +/** @deprecated */ +export interface TonFeeInfo extends BaseFeeInfo { + type: 'ton'; + options: { + slow: TonTipInfo; + average: TonTipInfo; + fast: TonTipInfo; + default: FeeDefaultOption; + } +} + +/** @deprecated */ +export type TonFeeDetail = TonFeeInfo & BaseFeeDetail; diff --git a/packages/extension-base/src/types/transaction/request.ts b/packages/extension-base/src/types/transaction/request.ts index 3f59aec5468..613287eac7c 100644 --- a/packages/extension-base/src/types/transaction/request.ts +++ b/packages/extension-base/src/types/transaction/request.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // eslint-disable-next-line @typescript-eslint/ban-types -import { TransactionWarningType } from '@subwallet/extension-base/types'; +import { TransactionFee, TransactionWarningType } from '@subwallet/extension-base/types'; export type BaseRequestSign = { ignoreWarnings?: TransactionWarningType[]; @@ -16,15 +16,16 @@ export interface RequestBaseTransfer { value?: string; transferAll?: boolean; transferBounceable?: boolean; + isTransferLocalTokenAndPayThatTokenAsFee?: boolean; } -export interface RequestCheckTransfer extends RequestBaseTransfer { +export interface RequestCheckTransfer extends RequestBaseTransfer, TransactionFee { networkKey: string, } export type RequestTransfer = InternalRequestSign; -export interface RequestCheckCrossChainTransfer extends RequestBaseTransfer { +export interface RequestCheckCrossChainTransfer extends RequestBaseTransfer, TransactionFee { value: string; originNetworkKey: string, destinationNetworkKey: string, @@ -32,3 +33,15 @@ export interface RequestCheckCrossChainTransfer extends RequestBaseTransfer { } export type RequestCrossChainTransfer = InternalRequestSign; + +export interface RequestGetTokensCanPayFee { + proxyId: string; + chain: string; + feeAmount?: string; +} + +export interface RequestGetAmountForPair { + nativeTokenFeeAmount: string, + nativeTokenSlug: string, + toTokenSlug: string +} diff --git a/packages/extension-base/src/utils/fee/combine.ts b/packages/extension-base/src/utils/fee/combine.ts new file mode 100644 index 00000000000..781647ea101 --- /dev/null +++ b/packages/extension-base/src/utils/fee/combine.ts @@ -0,0 +1,57 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeOption, SubstrateFeeInfo, SubstrateTipInfo } from '@subwallet/extension-base/types'; + +interface EvmFeeCombine { + gasPrice?: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; +} + +export const combineEthFee = (feeInfo: EvmFeeInfo, feeOptions?: FeeOption, feeCustom?: EvmEIP1559FeeOption): EvmFeeCombine => { + let maxFeePerGas: string | undefined; + let maxPriorityFeePerGas: string | undefined; + + if (feeOptions && feeOptions !== 'custom') { + maxFeePerGas = feeInfo.options?.[feeOptions].maxFeePerGas; + maxPriorityFeePerGas = feeInfo.options?.[feeOptions].maxPriorityFeePerGas; + } else if (feeOptions === 'custom' && feeCustom) { + maxFeePerGas = feeCustom.maxFeePerGas; + maxPriorityFeePerGas = feeCustom.maxPriorityFeePerGas; + } else { + maxFeePerGas = feeInfo.options?.[feeInfo.options.default].maxFeePerGas; + maxPriorityFeePerGas = feeInfo.options?.[feeInfo.options.default].maxPriorityFeePerGas; + } + + if (feeInfo.gasPrice) { + return { + gasPrice: feeInfo.gasPrice + }; + } else { + return { + maxFeePerGas, + maxPriorityFeePerGas + }; + } +}; + +interface SubstrateFeeCombine { + tip: string; +} + +export const combineSubstrateFee = (_fee: SubstrateFeeInfo, _feeOptions?: FeeOption, feeCustom?: SubstrateTipInfo): SubstrateFeeCombine => { + let tip: string; + + if (_feeOptions && _feeOptions !== 'custom') { + tip = _fee.options[_feeOptions].tip; + } else if (_feeOptions === 'custom' && feeCustom && 'tip' in feeCustom) { + tip = feeCustom.tip; + } else { + tip = _fee.options[_fee.options.default].tip; + } + + return { + tip + }; +}; diff --git a/packages/extension-base/src/utils/fee/index.ts b/packages/extension-base/src/utils/fee/index.ts new file mode 100644 index 00000000000..7c53fe93606 --- /dev/null +++ b/packages/extension-base/src/utils/fee/index.ts @@ -0,0 +1,5 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +export * from './combine'; +export * from './transfer'; diff --git a/packages/extension-base/src/utils/fee/transfer.ts b/packages/extension-base/src/utils/fee/transfer.ts new file mode 100644 index 00000000000..837eb6f9e33 --- /dev/null +++ b/packages/extension-base/src/utils/fee/transfer.ts @@ -0,0 +1,372 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { _ChainAsset, _ChainInfo } from '@subwallet/chain-list/types'; +import { AmountData } from '@subwallet/extension-base/background/KoniTypes'; +import { XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; +import { _isSnowBridgeXcm } from '@subwallet/extension-base/core/substrate/xcm-parser'; +import { getERC20TransactionObject, getEVMTransactionObject } from '@subwallet/extension-base/services/balance-service/transfer/smart-contract'; +import { createTransferExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/token'; +import { createTonTransaction } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; +import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; +import { isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; +import { _isPolygonChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; +import { _isPosChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/posBridge'; +import { _EvmApi, _SubstrateApi, _TonApi } from '@subwallet/extension-base/services/chain-service/types'; +import { _getContractAddressOfToken, _isChainEvmCompatible, _isChainTonCompatible, _isLocalToken, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; +import { isTonTransaction } from '@subwallet/extension-base/services/transaction-service/helpers'; +import { ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; +import { EvmEIP1559FeeOption, FeeChainType, FeeDetail, FeeInfo, SubstrateTipInfo, TransactionFee } from '@subwallet/extension-base/types'; +import { ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; +import { BN_ZERO } from '@subwallet/extension-base/utils'; +import { isTonAddress } from '@subwallet/keyring'; +import BigN from 'bignumber.js'; +import { TransactionConfig } from 'web3-core'; + +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { u8aToHex } from '@polkadot/util'; +import { addressToEvm, isEthereumAddress } from '@polkadot/util-crypto'; + +import { combineEthFee, combineSubstrateFee } from './combine'; + +export interface CalculateMaxTransferable extends TransactionFee { + address: string; + srcToken: _ChainAsset; + destToken?: _ChainAsset; + srcChain: _ChainInfo; + destChain: _ChainInfo; + substrateApi: _SubstrateApi; + evmApi: _EvmApi; + tonApi: _TonApi; + recalculateMaxTransferableSpecialCase: (transferInfo: ResponseSubscribeTransfer) => Promise; +} + +export const detectTransferTxType = (srcToken: _ChainAsset, srcChain: _ChainInfo, destChain: _ChainInfo): FeeChainType => { + const isXcmTransfer = srcChain.slug !== destChain.slug; + + if (isXcmTransfer) { + const isAvailBridgeFromEvm = _isPureEvmChain(srcChain) && isAvailChainBridge(destChain.slug); + const isSnowBridgeEvmTransfer = _isPureEvmChain(srcChain) && _isSnowBridgeXcm(srcChain, destChain) && !isAvailBridgeFromEvm; + const isPolygonBridgeTransfer = _isPolygonChainBridge(srcChain.slug, destChain.slug); + const isPosBridgeTransfer = _isPosChainBridge(srcChain.slug, destChain.slug); + + return (isAvailBridgeFromEvm || isSnowBridgeEvmTransfer || isPolygonBridgeTransfer || isPosBridgeTransfer) ? 'evm' : 'substrate'; + } else { + if (_isChainEvmCompatible(srcChain) && _isTokenTransferredByEvm(srcToken)) { + return 'evm'; + } else if (_isChainTonCompatible(srcChain) && _isTokenTransferredByTon(srcToken)) { + return 'ton'; + } else { + return 'substrate'; + } + } +}; + +export const calculateMaxTransferable = async (id: string, request: CalculateMaxTransferable, freeBalance: AmountData, fee: FeeInfo): Promise => { + const { destChain, recalculateMaxTransferableSpecialCase, srcChain } = request; + const isXcmTransfer = srcChain.slug !== destChain.slug; + + let maxTransferableAmount: ResponseSubscribeTransfer; + + if (isXcmTransfer) { + maxTransferableAmount = await calculateXCMMaxTransferable(id, request, freeBalance, fee); + } else { + maxTransferableAmount = await calculateTransferMaxTransferable(id, request, freeBalance, fee); + } + + return recalculateMaxTransferableSpecialCase(maxTransferableAmount); +}; + +export const calculateTransferMaxTransferable = async (id: string, request: CalculateMaxTransferable, freeBalance: AmountData, fee: FeeInfo): Promise => { + const { address, destChain, evmApi, feeCustom, feeOption, srcChain, srcToken, substrateApi, tonApi } = request; + const feeChainType = fee.type; + let estimatedFee: string; + let feeOptions: FeeDetail; + let maxTransferable = new BigN(freeBalance.value); + + const fakeAddress = '5DRewsYzhJqZXU3SRaWy1FSt5iDr875ao91aw5fjrJmDG4Ap'; // todo: move this + const substrateAddress = fakeAddress; // todo: move this + const evmAddress = u8aToHex(addressToEvm(fakeAddress)); // todo: move this + + const recipient = _isChainEvmCompatible(destChain) ? evmAddress : substrateAddress; + + try { + let transaction: ValidateTransactionResponseInput['transaction']; + + if (isEthereumAddress(address) && isEthereumAddress(recipient) && _isTokenTransferredByEvm(srcToken)) { + // todo: refactor: merge getERC20TransactionObject & getEVMTransactionObject + // Estimate with EVM API + if (_isTokenEvmSmartContract(srcToken) || _isLocalToken(srcToken)) { + [transaction] = await getERC20TransactionObject({ + assetAddress: _getContractAddressOfToken(srcToken), + chain: srcChain.slug, + evmApi, + feeCustom, + feeInfo: fee, + feeOption, + from: address, + to: recipient, + transferAll: false, + value: '0' + }); + } else { + [transaction] = await getEVMTransactionObject({ + chain: srcChain.slug, + evmApi, + feeCustom, + feeInfo: fee, + feeOption, + from: address, + to: recipient, + transferAll: false, + value: '0' + }); + } + } else if (isTonAddress(address) && _isTokenTransferredByTon(srcToken)) { + [transaction] = await createTonTransaction({ + tokenInfo: srcToken, + from: address, + to: address, + networkKey: srcChain.slug, + value: '0', + transferAll: false, // currently not used + tonApi + }); + } else { + [transaction] = await createTransferExtrinsic({ + transferAll: false, + value: '0', + from: address, + networkKey: srcChain.slug, + tokenInfo: srcToken, + to: recipient, + substrateApi + }); + } + + if (feeChainType === 'evm') { + // Calculate fee for evm transaction + const tx = transaction as TransactionConfig; + + const gasLimit = tx.gas?.toString() || (await evmApi.api.eth.estimateGas(tx)).toString(); + + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const combineFee = combineEthFee(fee, feeOption, _feeCustom); + + if (combineFee.maxFeePerGas) { + estimatedFee = new BigN(combineFee.maxFeePerGas).multipliedBy(gasLimit).toFixed(0); + } else { + estimatedFee = new BigN(combineFee.gasPrice || '0').multipliedBy(gasLimit).toFixed(0); + } + + feeOptions = { + ...fee, + estimatedFee, + gasLimit: gasLimit.toString() + }; + } else if (feeChainType === 'substrate') { + // Calculate fee for substrate transaction + try { + const mockTx = transaction as SubmittableExtrinsic<'promise'>; + const paymentInfo = await mockTx.paymentInfo(address); + + estimatedFee = paymentInfo?.partialFee?.toString() || '0'; + } catch (e) { + estimatedFee = '0'; + } + + const _feeCustom = feeCustom as SubstrateTipInfo; + + const tip = combineSubstrateFee(fee, feeOption, _feeCustom).tip; + + estimatedFee = new BigN(estimatedFee).plus(tip).toFixed(0); + + feeOptions = { + ...fee, + estimatedFee + }; + } else { + if (transaction && (isTonTransaction(transaction))) { + estimatedFee = transaction.estimateFee; + feeOptions = { + ...fee, + estimatedFee: estimatedFee + }; + } else { + // Not implemented yet + estimatedFee = '0'; + feeOptions = { + ...fee, + estimatedFee: '0' + }; + } + } + } catch (e) { + estimatedFee = '0'; + + if (fee.type === 'evm') { + feeOptions = { + ...fee, + estimatedFee, + gasLimit: '0' + }; + } else { + feeOptions = { + ...fee, + estimatedFee + }; + } + + console.warn('Unable to estimate fee', e); + } + + maxTransferable = maxTransferable + .minus(new BigN(estimatedFee)); + + return { + maxTransferable: !_isNativeToken(srcToken) + ? freeBalance.value + : maxTransferable.gt(BN_ZERO) ? (maxTransferable.toFixed(0) || '0') : '0', + feeOptions: feeOptions, + feeType: feeChainType, + id: id + }; +}; + +export const calculateXCMMaxTransferable = async (id: string, request: CalculateMaxTransferable, freeBalance: AmountData, fee: FeeInfo): Promise => { + const { address, destChain, destToken, evmApi, feeCustom, feeOption, srcChain, srcToken, substrateApi } = request; + const feeChainType = fee.type; + let estimatedFee: string; + let feeOptions: FeeDetail; + let maxTransferable = new BigN(freeBalance.value); + + const isAvailBridgeFromEvm = _isPureEvmChain(srcChain) && isAvailChainBridge(destChain.slug); + const isAvailBridgeFromAvail = isAvailChainBridge(srcChain.slug) && _isPureEvmChain(destChain); + const isSnowBridgeEvmTransfer = _isPureEvmChain(srcChain) && _isSnowBridgeXcm(srcChain, destChain) && !isAvailBridgeFromEvm; + const isPolygonBridgeTransfer = _isPolygonChainBridge(srcChain.slug, destChain.slug); + const isPosBridgeTransfer = _isPosChainBridge(srcChain.slug, destChain.slug); + + const fakeAddress = '5DRewsYzhJqZXU3SRaWy1FSt5iDr875ao91aw5fjrJmDG4Ap'; // todo: move this + const substrateAddress = fakeAddress; // todo: move this + const evmAddress = u8aToHex(addressToEvm(fakeAddress)); // todo: move this + + const recipient = _isChainEvmCompatible(destChain) ? evmAddress : substrateAddress; + + try { + if (!destToken) { + maxTransferable = BN_ZERO; + + throw Error('Destination token is not available'); + } + + const params: CreateXcmExtrinsicProps = { + destinationTokenInfo: destToken, + originTokenInfo: srcToken, + // If value is 0, substrate will throw error when estimating fee + sendingValue: feeChainType === 'substrate' ? '1000000000000000000' : '0', + sender: address, + recipient, + destinationChain: destChain, + originChain: srcChain, + substrateApi, + evmApi, + feeCustom, + feeOption, + feeInfo: fee + }; + + let funcCreateExtrinsic: FunctionCreateXcmExtrinsic; + + if (isPosBridgeTransfer || isPolygonBridgeTransfer) { + funcCreateExtrinsic = createPolygonBridgeExtrinsic; + } else if (isSnowBridgeEvmTransfer) { + funcCreateExtrinsic = createSnowBridgeExtrinsic; + } else if (isAvailBridgeFromEvm) { + funcCreateExtrinsic = createAvailBridgeTxFromEth; + } else if (isAvailBridgeFromAvail) { + funcCreateExtrinsic = createAvailBridgeExtrinsicFromAvail; + } else { + funcCreateExtrinsic = createXcmExtrinsic; + } + + const extrinsic = await funcCreateExtrinsic(params); + + if (feeChainType === 'evm') { + // Calculate fee for evm transaction + const tx = extrinsic as TransactionConfig; + + const gasLimit = tx.gas?.toString() || (await evmApi.api.eth.estimateGas(tx)).toString(); + + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const combineFee = combineEthFee(fee, feeOption, _feeCustom); + + if (combineFee.maxFeePerGas) { + estimatedFee = new BigN(combineFee.maxFeePerGas).multipliedBy(gasLimit).toFixed(0); + } else { + estimatedFee = new BigN(combineFee.gasPrice || '0').multipliedBy(gasLimit).toFixed(0); + } + + feeOptions = { + ...fee, + estimatedFee, + gasLimit: gasLimit.toString() + }; + } else if (feeChainType === 'substrate') { + // Calculate fee for substrate transaction + try { + const paymentInfo = await (extrinsic as SubmittableExtrinsic<'promise'>).paymentInfo(address); + + estimatedFee = paymentInfo?.partialFee?.toString() || '0'; + } catch (e) { + estimatedFee = '0'; + } + + const _feeCustom = feeCustom as SubstrateTipInfo; + + const tip = combineSubstrateFee(fee, feeOption, _feeCustom).tip; + + estimatedFee = new BigN(estimatedFee).plus(tip).toFixed(0); + + feeOptions = { + ...fee, + estimatedFee + }; + } else { + // Not implemented yet + estimatedFee = '0'; + feeOptions = { + ...fee, + estimatedFee: '0' + }; + } + } catch (e) { + estimatedFee = '0'; + + if (fee.type === 'evm') { + feeOptions = { + ...fee, + estimatedFee, + gasLimit: '0' + }; + } else { + feeOptions = { + ...fee, + estimatedFee + }; + } + + console.warn('Unable to estimate fee', e); + } + + maxTransferable = maxTransferable + .minus(new BigN(estimatedFee).multipliedBy(XCM_FEE_RATIO)); + + return { + maxTransferable: !_isNativeToken(srcToken) + ? freeBalance.value + : maxTransferable.gt(BN_ZERO) ? (maxTransferable.toFixed(0) || '0') : '0', + feeOptions: feeOptions, + feeType: feeChainType, + id: id + }; +}; diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index 63cccdd65da..4d0f8da2339 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -396,6 +396,7 @@ export * from './asset'; export * from './auth'; export * from './environment'; export * from './eth'; +export * from './fee'; export * from './fetchEvmChainInfo'; export * from './fetchStaticData'; export * from './gear'; diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/TransferBlock.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/TransferBlock.tsx index 11c44773369..c7b6f72f7d7 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/TransferBlock.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/Transaction/variants/TransferBlock.tsx @@ -29,7 +29,8 @@ const Component: React.FC = ({ className, transaction }: Props) => { [chainInfoMap, transaction.chain] ); - const { decimals: chainDecimals, symbol: chainSymbol } = useGetNativeTokenBasicInfo(transaction.chain); + const { decimals: nativeTokenDecimals, symbol: nativeTokenSymbol } = useGetNativeTokenBasicInfo(transaction.chain); + const feeInfo = transaction.estimateFee; return ( <> @@ -84,10 +85,10 @@ const Component: React.FC = ({ className, transaction }: Props) => { /> { diff --git a/packages/extension-koni-ui/src/Popup/Transaction/Transaction.tsx b/packages/extension-koni-ui/src/Popup/Transaction/Transaction.tsx index c994190041c..4c42ccb6cde 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/Transaction.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/Transaction.tsx @@ -173,7 +173,7 @@ function Component ({ className }: Props) { closeRecheckChainConnectionModal }} > - +
(); const { defaultSlug: sendFundSlug } = defaultData; const isFirstRender = useIsFirstRender(); + const priceMap = useSelector((state) => state.price.priceMap); const [form] = Form.useForm(); const formDefault = useMemo((): TransferParams => { @@ -148,17 +152,22 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const fromValue = useWatchTransaction('from', form, defaultData); const chainValue = useWatchTransaction('chain', form, defaultData); const assetValue = useWatchTransaction('asset', form, defaultData); - + const { nativeTokenBalance } = useGetBalance(chainValue, fromValue); const assetInfo = useFetchChainAssetInfo(assetValue); const { alertProps, closeAlert, openAlert } = useAlert(alertModalId); const { chainInfoMap, chainStateMap, chainStatusMap, ledgerGenericAllowNetworks } = useSelector((root) => root.chainStore); const { assetRegistry, xcmRefMap } = useSelector((root) => root.assetRegistry); const { accounts } = useSelector((state: RootState) => state.accountState); - const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); + const { accountProxies, currentAccountProxy } = useSelector((state: RootState) => state.accountState); const [autoFormatValue] = useLocalStorage(ADDRESS_INPUT_AUTO_FORMAT_VALUE, false); + const [listTokensCanPayFee, setListTokensCanPayFee] = useState([]); + + // TODO: Should manage the states `tokenPayFeeAmount` and `currentTokenPayFee` together. + const [nonNativeTokenPayFeeAmount, setNonNativeTokenPayFeeAmount] = useState(undefined); + const [currentNonNativeTokenPayFee, setCurrentNonNativeTokenPayFee] = useState(undefined); - const [maxTransfer, setMaxTransfer] = useState('0'); + const [selectedTransactionFee, setSelectedTransactionFee] = useState(); const { getCurrentConfirmation, renderConfirmationButtons } = useGetConfirmationByScreen('send-fund'); const checkAction = usePreCheckAction(fromValue, true, detectTranslate('The account you are using is {{accountTitle}}, you cannot send assets with it')); @@ -181,11 +190,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone }, [chainInfoMap, chainValue, destChainValue, assetInfo]); const disabledToAddressInput = useMemo(() => { - if (_isPosChainL2Bridge(chainValue, destChainValue)) { - return true; - } - - return false; + return _isPosChainL2Bridge(chainValue, destChainValue); }, [chainValue, destChainValue]); const [loading, setLoading] = useState(false); @@ -195,10 +200,16 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const [addressInputRenderKey, setAddressInputRenderKey] = useState(defaultAddressInputRenderKey); const [, update] = useState({}); - const [isFetchingMaxValue, setIsFetchingMaxValue] = useState(false); const [isBalanceReady, setIsBalanceReady] = useState(true); const [forceUpdateMaxValue, setForceUpdateMaxValue] = useState(undefined); + const [transferInfo, setTransferInfo] = useState(); + const [isFetchingInfo, setIsFetchingInfo] = useState(false); const chainStatus = useMemo(() => chainStatusMap[chainValue]?.connectionStatus, [chainValue, chainStatusMap]); + const estimatedNativeFee = useMemo((): string => transferInfo?.feeOptions.estimatedFee || '0', [transferInfo]); + const [isTransferLocalTokenAndPayThatTokenAsFee, setIsTransferLocalTokenAndPayThatTokenAsFee] = useState(false); + + // todo: remove after testing + console.log('[TESTER] Transfer info:', transferInfo, 'priceMap: ', priceMap); const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); @@ -330,6 +341,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone }, [accounts, autoFormatValue, chainInfoMap, form, ledgerGenericAllowNetworks]); const validateAmount = useCallback((rule: Rule, amount: string): Promise => { + const maxTransfer = transferInfo?.maxTransferable || '0'; + if (!amount) { return Promise.reject(t('Amount is required')); } @@ -349,7 +362,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone } return Promise.resolve(); - }, [decimals, maxTransfer, t]); + }, [decimals, t, transferInfo?.maxTransferable]); const onValuesChange: FormCallbacks['onValuesChange'] = useCallback( (part: Partial, values: TransferParams) => { @@ -367,6 +380,10 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone setAddressInputRenderKey(`${defaultAddressInputRenderKey}-${Date.now()}`); setIsTransferAll(false); setForceUpdateMaxValue(undefined); + + setCurrentNonNativeTokenPayFee(undefined); + setNonNativeTokenPayFeeAmount(undefined); + setIsTransferLocalTokenAndPayThatTokenAsFee(false); } if (part.destChain || part.chain || part.value || part.asset) { @@ -451,12 +468,16 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone // Transfer token or send fund sendPromise = makeTransfer({ from, - networkKey: chain, + chain, to: to, tokenSlug: asset, value: value, transferAll: options.isTransferAll, - transferBounceable: options.isTransferBounceable + transferBounceable: options.isTransferBounceable, + isTransferLocalTokenAndPayThatTokenAsFee: options.isTransferLocalTokenAndPayThatTokenAsFee, + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom, + nonNativeTokenPayFeeSlug: currentNonNativeTokenPayFee }); } else { // Make cross chain transfer @@ -468,12 +489,16 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone to, value, transferAll: options.isTransferAll, - transferBounceable: options.isTransferBounceable + transferBounceable: options.isTransferBounceable, + isTransferLocalTokenAndPayThatTokenAsFee: options.isTransferLocalTokenAndPayThatTokenAsFee, + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom, + nonNativeTokenPayFeeSlug: currentNonNativeTokenPayFee }); } return sendPromise; - }, []); + }, [selectedTransactionFee, currentNonNativeTokenPayFee]); // todo: must refactor later, temporary solution to support SnowBridge const handleBridgeSpendingApproval = useCallback((values: TransferParams): Promise => { @@ -550,17 +575,28 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone }, [handleBasicSubmit, handleBridgeSpendingApproval, isShowWarningOnSubmit, onError, onSuccess, processState]); const onSetMaxTransferable = useCallback((value: boolean) => { - const bnMaxTransfer = new BN(maxTransfer); + const bnMaxTransfer = new BN(transferInfo?.maxTransferable || '0'); if (!bnMaxTransfer.isZero()) { setIsTransferAll(value); } - }, [maxTransfer]); + }, [transferInfo?.maxTransferable]); + + const onSetTokenPayFee = useCallback((slug: string) => { + setCurrentNonNativeTokenPayFee(slug); + }, [setCurrentNonNativeTokenPayFee]); + + const nativeTokenSlug = useMemo(() => { + const chainInfo = chainInfoMap[chainValue]; + + return chainInfo && _getChainNativeTokenSlug(chainInfo); + }, [chainInfoMap, chainValue]); const onSubmit: FormCallbacks['onFinish'] = useCallback((values: TransferParams) => { const options: TransferOptions = { isTransferAll: isTransferAll, - isTransferBounceable: false + isTransferBounceable: false, + isTransferLocalTokenAndPayThatTokenAsFee }; let checkTransferAll = false; @@ -678,7 +714,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone _doSubmit().catch((error) => { console.error('Error during submit:', error); }); - }, [assetInfo, chainInfoMap, closeAlert, doSubmit, form, isTransferAll, openAlert, t, updateAddressInputValue]); + }, [assetInfo, chainInfoMap, closeAlert, doSubmit, form, isTransferAll, isTransferLocalTokenAndPayThatTokenAsFee, openAlert, t, updateAddressInputValue]); const onClickSubmit = useCallback((values: TransferParams) => { if (currentConfirmation) { @@ -754,52 +790,70 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone useEffect(() => { let cancel = false; - setIsFetchingMaxValue(false); + // setIsFetchingMaxValue(false); + + let id = ''; + + setIsFetchingInfo(true); + + const validate = () => { + const value = form.getFieldValue('value') as string; + + if (value) { + setTimeout(() => { + form.validateFields(['value']).finally(() => update({})); + }, 100); + } + }; + + const callback = (transferInfo: ResponseSubscribeTransfer) => { + if (!cancel) { + setTransferInfo(transferInfo); + + id = transferInfo.id; + + validate(); + } else { + cancelSubscription(transferInfo.id).catch(console.error); + } + }; if (fromValue && assetValue) { - getMaxTransfer({ + subscribeMaxTransfer({ address: fromValue, - networkKey: assetRegistry[assetValue].originChain, + chain: assetRegistry[assetValue].originChain, token: assetValue, - isXcmTransfer: chainValue !== destChainValue, - destChain: destChainValue - }) - .then((balance) => { - if (!cancel) { - setMaxTransfer(balance.value); - setIsFetchingMaxValue(true); - } - }) - .catch(() => { - if (!cancel) { - setMaxTransfer('0'); - setIsFetchingMaxValue(true); - } + destChain: destChainValue, + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom, + nonNativeTokenPayFeeSlug: currentNonNativeTokenPayFee + }, callback) + .then((callback)) + .catch((e) => { + console.error('Error in subscribeMaxTransfer:', e); + + setTransferInfo(undefined); + validate(); }) .finally(() => { - if (!cancel) { - const value = form.getFieldValue('value') as string; - - if (value) { - update({}); - } - } + setIsFetchingInfo(false); }); } return () => { cancel = true; + id && cancelSubscription(id).catch(console.error); }; - }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue]); + }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue, selectedTransactionFee, isTransferLocalTokenAndPayThatTokenAsFee, nativeTokenSlug, currentNonNativeTokenPayFee]); useEffect(() => { const bnTransferAmount = new BN(transferAmountValue || '0'); - const bnMaxTransfer = new BN(maxTransfer || '0'); + const bnMaxTransfer = new BN(transferInfo?.maxTransferable || '0'); if (bnTransferAmount.gt(BN_ZERO) && bnTransferAmount.eq(bnMaxTransfer)) { setIsTransferAll(true); } - }, [maxTransfer, transferAmountValue]); + }, [transferAmountValue, transferInfo?.maxTransferable]); useEffect(() => { getOptimalTransferProcess({ @@ -840,6 +894,58 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone } }, [accountAddressItems, addressInputCurrent, chainInfoMap, chainValue, disabledToAddressInput, form, fromValue]); + useEffect(() => { + const fetchTokens = async () => { + try { + const _response = await getTokensCanPayFee({ + chain: chainValue, + proxyId: currentAccountProxy?.id || '' + }); + + const response = _response.filter((item) => item !== null && item !== undefined); + + setListTokensCanPayFee(response); + } catch (error) { + console.error('Error fetching tokens:', error); + } + }; + + fetchTokens().catch((error) => { + console.error('Unhandled error in fetchTokens:', error); + }); + }, [chainValue, currentAccountProxy?.id, fromValue, nativeTokenBalance, nativeTokenSlug]); + + useEffect(() => { + if (currentNonNativeTokenPayFee && currentNonNativeTokenPayFee !== nativeTokenSlug) { + const getEstimatedFee = async () => { + try { + const tokenPayFeeAmount = await getAmountForPair({ + nativeTokenSlug, + nativeTokenFeeAmount: estimatedNativeFee, + toTokenSlug: currentNonNativeTokenPayFee + }); + + setNonNativeTokenPayFeeAmount(tokenPayFeeAmount); + } catch (error) { + console.error('Error fetching tokens:', error); + } + }; + + getEstimatedFee().catch((error) => { + console.error('Unhandled error in getEstimatedFee:', error); + }); + } + }, [chainInfoMap, chainValue, currentNonNativeTokenPayFee, estimatedNativeFee, nativeTokenSlug]); + + useEffect(() => { + const isNonNativeToken = assetValue && !assetValue.includes(_AssetType.NATIVE) && currentNonNativeTokenPayFee && !currentNonNativeTokenPayFee.includes(_AssetType.NATIVE); + const isSameToken = assetValue === currentNonNativeTokenPayFee; + + if (isNonNativeToken && isSameToken) { + setIsTransferLocalTokenAndPayThatTokenAsFee(true); + } + }, [assetValue, currentNonNativeTokenPayFee]); + useRestoreTransaction(form); return ( @@ -945,7 +1051,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone decimals={decimals} disabled={decimals === 0} forceUpdateMaxValue={forceUpdateMaxValue} - maxValue={maxTransfer} + maxValue={transferInfo?.maxTransferable || '0'} onSetMax={onSetMaxTransferable} showMaxButton={!hideMaxButton} tooltip={t('Amount')} @@ -953,6 +1059,20 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone + {!TON_CHAINS.includes(chainValue) && nativeTokenSlug && ()} { chainValue !== destChainValue && (
@@ -977,7 +1097,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone className={`${className} -transaction-footer`} > - )} - id={modalId} - onCancel={onCancelModal} - title={t('Choose fee')} - > -
- -
+ return Promise.resolve(); + }, [t]); -
-
- {t('Fee paid in')} -
-
+ const customMaxFeeValidator = useCallback((rule: Rule, value: string): Promise => { + if (!value) { + return Promise.reject(t('Please enter the maximum fee.')); + } - { - currentViewMode === ViewMode.RECOMMENDED && ( -
- {OPTIONS.map(renderOption)} -
- ) + if (feeOptionsInfo && 'baseGasFee' in feeOptionsInfo) { + const baseGasFee = feeOptionsInfo.baseGasFee; + const maxFeeValue = form.getFieldValue('maxFeeValue') as string; + + if (baseGasFee && maxFeeValue && new BigN(value).lte(new BigN(baseGasFee).multipliedBy(1.5))) { + return Promise.reject(t('The maximum fee entered is too low')); } - { - currentViewMode === ViewMode.CUSTOM && ( -
-
-
- - ['onValuesChange'] = useCallback( + (part: Partial, values: FormProps) => { + if (part.customValue) { + form.setFieldsValue({ + customValue: part.customValue + }); + } + }, + [form] + ); + + const onClickEdit = useCallback(() => { + setTimeout(() => { + activeModal(CHOOSE_FEE_TOKEN_MODAL); + }, 100); + }, [activeModal]); + + const onClickSubmit = useCallback(() => { + if (optionSelected) { + onSelectOption(optionSelected); + } + + inactiveModal(modalId); + form.submit(); + }, [form, inactiveModal, modalId, onSelectOption, optionSelected]); + + const renderCustomValueField = () => ( +
+ + + + +
+ ); + + const renderEvmFeeFields = () => ( +
+ + + + + + +
+ ); + + return ( + <> + + {t('Approve')} + + )} + id={modalId} + onCancel={onCancelModal} + rightIconProps={{ + icon: ( + + ), + onClick: onCancelModal + }} + title={t('Choose fee')} + > + {feeType === 'evm' && ( +
+ +
+ )} + +
+
{t('Fee paid in')}
+
+ +
{symbol}
+ {feeType !== 'evm' + ? ( +
- - -
- +
) + : undefined}
- ) - } -
+
+ + { + currentViewMode === ViewMode.RECOMMENDED && ( +
+ {OPTIONS.map(renderOption)} +
+ ) + } + + { + currentViewMode === ViewMode.CUSTOM && ( +
+
+ {feeType === 'evm' + ? ( + renderEvmFeeFields() + ) + : chainValue && ASSET_HUB_CHAIN_SLUGS.includes(chainValue) + ? null + : ( + renderCustomValueField() + )} + +
+
+ ) + } + + {/* */} + ); }; @@ -213,10 +423,27 @@ export const FeeEditorModal = styled(Component)(({ theme: { token } }: Pr paddingBottom: 0 }, + '&.fee-editor-modal': { + '.ant-sw-sub-header-container.ant-sw-sub-header-container': { + display: 'flex', + flexDirection: 'row-reverse' + }, + '.ant-sw-header-left-part': { + paddingRight: token.paddingXS + }, + '.ant-sw-header-right-part': { + paddingLeft: token.paddingXS + } + }, + '.ant-sw-modal-footer': { borderTop: 0 }, + '.__base-fee-value-field.__base-fee-value-field': { + marginBottom: 8 + }, + '.__switcher-box': { marginBottom: token.margin }, @@ -225,7 +452,25 @@ export const FeeEditorModal = styled(Component)(({ theme: { token } }: Pr padding: token.paddingSM, backgroundColor: token.colorBgSecondary, borderRadius: token.borderRadiusLG, - marginBottom: token.marginXS + marginBottom: token.marginXS, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between' + }, + + '.__fee-paid-token': { + display: 'flex', + alignItems: 'center' + }, + + '.__fee-paid-token-symbol': { + paddingLeft: 8, + color: token.colorWhite + }, + + '.__edit-token': { + paddingLeft: 4, + cursor: 'pointer' }, '.__fee-token-selector-label': { diff --git a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeOptionItem.tsx b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeOptionItem.tsx index d78ddad3177..85295d3d759 100644 --- a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeOptionItem.tsx +++ b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeOptionItem.tsx @@ -78,7 +78,7 @@ const Component: React.FC = (props: Props) => { timeString = timeString.trim(); - return timeString ? `~ ${timeString}` : '0 min'; // Return '0 minutes' if time is 0 + return timeString ? `~ ${timeString}` : `${seconds} sec`; // Return '0 minutes' if time is 0 } else { return 'Unknown time'; } diff --git a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx index ed11d81b7b8..f1ba4ca6b01 100644 --- a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx +++ b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/index.tsx @@ -2,11 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { _getAssetDecimals, _getAssetPriceId, _getAssetSymbol } from '@subwallet/extension-base/services/chain-service/utils'; +import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; +import { FeeDetail, TransactionFee } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; +import ChooseFeeTokenModal from '@subwallet/extension-koni-ui/components/Field/TransactionFee/FeeEditor/ChooseFeeTokenModal'; +import { ASSET_HUB_CHAIN_SLUGS, BN_TEN, CHOOSE_FEE_TOKEN_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { Icon, ModalContext, Number } from '@subwallet/react-ui'; +import { Icon, ModalContext, Number, Tooltip } from '@subwallet/react-ui'; import BigN from 'bignumber.js'; +import CN from 'classnames'; import { PencilSimpleLine } from 'phosphor-react'; import React, { useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -27,16 +32,26 @@ export type RenderFieldNodeParams = { } type Props = ThemeProps & { - onSelect?: () => void; + onSelect?: (option: TransactionFee) => void; isLoading?: boolean; tokenSlug: string; + feeOptionsInfo?: FeeDetail; + estimateFee: string; renderFieldNode?: (params: RenderFieldNodeParams) => React.ReactNode; + feeType?: string; + loading?: boolean; + listTokensCanPayFee: TokenHasBalanceInfo[]; + onSetTokenPayFee: (slug: string) => void; + currentTokenPayFee?: string; + chainValue?: string; + destChainValue?: string; + selectedFeeOption?: TransactionFee }; // todo: will update dynamic later const modalId = 'FeeEditorModalId'; -const Component = ({ className, isLoading = false, onSelect, renderFieldNode, tokenSlug }: Props): React.ReactElement => { +const Component = ({ chainValue, className, currentTokenPayFee, destChainValue, estimateFee, feeOptionsInfo, feeType, isLoading = false, listTokensCanPayFee, loading, onSelect, onSetTokenPayFee, renderFieldNode, selectedFeeOption, tokenSlug }: Props): React.ReactElement => { const { t } = useTranslation(); const { activeModal } = useContext(ModalContext); const assetRegistry = useSelector((root) => root.assetRegistry.assetRegistry); @@ -50,6 +65,7 @@ const Component = ({ className, isLoading = false, onSelect, renderFieldNode, to const decimals = _getAssetDecimals(tokenAsset); // @ts-ignore const priceId = _getAssetPriceId(tokenAsset); + const priceValue = priceMap[priceId] || 0; const symbol = _getAssetSymbol(tokenAsset); const feeValue = useMemo(() => { @@ -60,14 +76,25 @@ const Component = ({ className, isLoading = false, onSelect, renderFieldNode, to return BN_ZERO; }, []); + const convertedFeeValueToUSD = useMemo(() => { + return new BigN(estimateFee) + .multipliedBy(priceValue) + .dividedBy(BN_TEN.pow(decimals || 0)) + .toNumber(); + }, [decimals, estimateFee, priceValue]); + const onClickEdit = useCallback(() => { setTimeout(() => { - activeModal(modalId); + if (chainValue && ASSET_HUB_CHAIN_SLUGS.includes(chainValue)) { + activeModal(CHOOSE_FEE_TOKEN_MODAL); + } else { + activeModal(modalId); + } }, 100); - }, [activeModal]); + }, [activeModal, chainValue]); - const onSelectOption = useCallback(() => { - onSelect?.(); + const onSelectTransactionFee = useCallback((fee: TransactionFee) => { + onSelect?.(fee); }, [onSelect]); const customFieldNode = useMemo(() => { @@ -88,11 +115,17 @@ const Component = ({ className, isLoading = false, onSelect, renderFieldNode, to }); }, [decimals, feeValue, isLoading, onClickEdit, renderFieldNode, symbol, feePriceValue]); + const isEditButton = useMemo(() => { + const isXcm = chainValue && destChainValue && chainValue !== destChainValue; + + return !!(chainValue && listTokensCanPayFee.length > 0 && (ASSET_HUB_CHAIN_SLUGS.includes(chainValue) || feeType === 'evm')) && !isXcm; + }, [chainValue, destChainValue, feeType, listTokensCanPayFee?.length]); + return ( <> { customFieldNode || ( -
+
{t('Estimate fee')}: @@ -102,35 +135,61 @@ const Component = ({ className, isLoading = false, onSelect, renderFieldNode, to className={'__fee-value'} decimal={decimals} suffix={symbol} - value={feeValue} + value={estimateFee} />
-
-
- - - + {feeType !== 'ton' && ( +
+
+ + +
+ +
+
+
-
+ )}
) } + + ); @@ -152,6 +211,16 @@ const FeeEditor = styled(Component)(({ theme: { token } }: Props) => { } }, + '&.__estimate-fee-wrapper': { + backgroundColor: token.colorBgSecondary, + padding: token.paddingSM, + height: token.sizeXXL, + borderRadius: token.borderRadiusLG, + '.__edit-icon': { + color: token['gray-5'] + } + }, + '.__field-left-part': { flex: 1, display: 'flex', diff --git a/packages/extension-koni-ui/src/constants/chain.ts b/packages/extension-koni-ui/src/constants/chain.ts new file mode 100644 index 00000000000..370efd4b884 --- /dev/null +++ b/packages/extension-koni-ui/src/constants/chain.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export const ASSET_HUB_CHAIN_SLUGS = ['paseo_assethub', 'westend_assethub', 'rococo_assethub', 'statemine', 'statemint']; diff --git a/packages/extension-koni-ui/src/constants/index.ts b/packages/extension-koni-ui/src/constants/index.ts index 0b557e10f4d..12da55e6f0b 100644 --- a/packages/extension-koni-ui/src/constants/index.ts +++ b/packages/extension-koni-ui/src/constants/index.ts @@ -19,3 +19,4 @@ export * from './strings'; export * from './transaction'; export * from './earning'; export * from './session'; +export * from './chain'; diff --git a/packages/extension-koni-ui/src/constants/modal.ts b/packages/extension-koni-ui/src/constants/modal.ts index 0035d0bbebb..5c2e74b7f89 100644 --- a/packages/extension-koni-ui/src/constants/modal.ts +++ b/packages/extension-koni-ui/src/constants/modal.ts @@ -39,7 +39,7 @@ export const ACCOUNT_NAME_MODAL = 'account-name-modal'; export const GLOBAL_ALERT_MODAL = 'global-alert-modal'; export const TON_WALLET_CONTRACT_SELECTOR_MODAL = 'ton-wallet-contract-selector-modal'; export const TON_ACCOUNT_SELECTOR_MODAL = 'ton-account-selector-modal'; - +export const CHOOSE_FEE_TOKEN_MODAL = 'choose-fee-token-modal'; /* Campaign */ export const HOME_CAMPAIGN_BANNER_MODAL = 'home-campaign-banner-modal'; /* Campaign */ diff --git a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts index 5a431cda98b..dd2103b1e0a 100644 --- a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts +++ b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts @@ -3,13 +3,15 @@ import { AmountData, RequestMaxTransferable } from '@subwallet/extension-base/background/KoniTypes'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers'; +import { TokenHasBalanceInfo } from '@subwallet/extension-base/services/fee-service/interfaces'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { RequestCrossChainTransfer, RequestTransfer, TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; +import { RequestCrossChainTransfer, RequestGetAmountForPair, RequestGetTokensCanPayFee, TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; +import { RequestSubmitTransfer, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { CommonOptimalPath } from '@subwallet/extension-base/types/service-base'; import { sendMessage } from '../base'; -export async function makeTransfer (request: RequestTransfer): Promise { +export async function makeTransfer (request: RequestSubmitTransfer): Promise { return sendMessage('pri(accounts.transfer)', request); } @@ -25,6 +27,18 @@ export async function getMaxTransfer (request: RequestMaxTransferable): Promise< return sendMessage('pri(transfer.getMaxTransferable)', request); } +export async function subscribeMaxTransfer (request: RequestSubscribeTransfer, callback: (data: ResponseSubscribeTransfer) => void): Promise { + return sendMessage('pri(transfer.subscribe)', request, callback); +} + export async function getOptimalTransferProcess (request: RequestOptimalTransferProcess): Promise { return sendMessage('pri(accounts.getOptimalTransferProcess)', request); } + +export async function getTokensCanPayFee (request: RequestGetTokensCanPayFee): Promise { // can set a default fee to ED of native token + return sendMessage('pri(customFee.getTokensCanPayFee)', request); +} + +export async function getAmountForPair (request: RequestGetAmountForPair): Promise { + return sendMessage('pri(customFee.getAmountForPair)', request); +} diff --git a/patches/@polkadot+types+15.0.1.patch b/patches/@polkadot+types+15.0.1.patch new file mode 100644 index 00000000000..dacb6882392 --- /dev/null +++ b/patches/@polkadot+types+15.0.1.patch @@ -0,0 +1,51 @@ +diff --git a/node_modules/@polkadot/types/cjs/extrinsic/v4/ExtrinsicPayload.js b/node_modules/@polkadot/types/cjs/extrinsic/v4/ExtrinsicPayload.js +index 200193c..f5d639a 100644 +--- a/node_modules/@polkadot/types/cjs/extrinsic/v4/ExtrinsicPayload.js ++++ b/node_modules/@polkadot/types/cjs/extrinsic/v4/ExtrinsicPayload.js +@@ -13,7 +13,16 @@ const util_js_1 = require("../util.js"); + class GenericExtrinsicPayloadV4 extends types_codec_1.Struct { + __internal__signOptions; + constructor(registry, value) { +- super(registry, (0, util_1.objectSpread)({ method: 'Bytes' }, registry.getSignedExtensionTypes(), registry.getSignedExtensionExtra()), value); ++ let _value = value; ++ if (value && value.assetId && (0, util_1.isHex)(value.assetId)) { ++ const assetId = registry.createType('TAssetConversion', (0, util_1.hexToU8a)(value.assetId)); ++ // we only want to adjust the payload if the hex passed has the option ++ if (value.assetId === '0x00' || ++ value.assetId === '0x01' + assetId.toHex().slice(2)) { ++ _value = Object.assign({}, value, { assetId: assetId.toJSON() }); ++ } ++ } ++ super(registry, (0, util_1.objectSpread)({ method: 'Bytes' }, registry.getSignedExtensionTypes(), registry.getSignedExtensionExtra()), _value); + // Do detection for the type of extrinsic, in the case of MultiSignature + // this is an enum, in the case of AnySignature, this is a Hash only + // (which may be 64 or 65 bytes) +diff --git a/node_modules/@polkadot/types/extrinsic/v4/ExtrinsicPayload.js b/node_modules/@polkadot/types/extrinsic/v4/ExtrinsicPayload.js +index 942539e..4b2330a 100644 +--- a/node_modules/@polkadot/types/extrinsic/v4/ExtrinsicPayload.js ++++ b/node_modules/@polkadot/types/extrinsic/v4/ExtrinsicPayload.js +@@ -1,5 +1,5 @@ + import { Enum, Struct } from '@polkadot/types-codec'; +-import { objectSpread } from '@polkadot/util'; ++import { hexToU8a, isHex, objectSpread } from '@polkadot/util'; + import { sign } from '../util.js'; + /** + * @name GenericExtrinsicPayloadV4 +@@ -10,7 +10,16 @@ import { sign } from '../util.js'; + export class GenericExtrinsicPayloadV4 extends Struct { + __internal__signOptions; + constructor(registry, value) { +- super(registry, objectSpread({ method: 'Bytes' }, registry.getSignedExtensionTypes(), registry.getSignedExtensionExtra()), value); ++ let _value = value; ++ if (value && value.assetId && isHex(value.assetId)) { ++ const assetId = registry.createType('TAssetConversion', hexToU8a(value.assetId)); ++ // we only want to adjust the payload if the hex passed has the option ++ if (value.assetId === '0x00' || ++ value.assetId === '0x01' + assetId.toHex().slice(2)) { ++ _value = Object.assign({}, value, { assetId: assetId.toJSON() }); ++ } ++ } ++ super(registry, objectSpread({ method: 'Bytes' }, registry.getSignedExtensionTypes(), registry.getSignedExtensionExtra()), _value); + // Do detection for the type of extrinsic, in the case of MultiSignature + // this is an enum, in the case of AnySignature, this is a Hash only + // (which may be 64 or 65 bytes)