From 3758f4605bdab58daea301c5e96145e6c1fb2331 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 10 Jan 2025 19:14:20 +0700 Subject: [PATCH 1/9] [Issue-3658] feat: Add Interface to support custom fee transactions --- .../src/background/KoniTypes.ts | 3 +- .../src/services/transaction-service/types.ts | 8 +-- .../src/types/balance/transfer.ts | 31 ++++++++++++ packages/extension-base/src/types/fee/base.ts | 13 +++++ packages/extension-base/src/types/fee/evm.ts | 49 +++++++++++++------ .../extension-base/src/types/fee/index.ts | 5 +- .../extension-base/src/types/fee/option.ts | 12 +++++ .../src/types/fee/subscription.ts | 20 ++++++++ .../extension-base/src/types/fee/substrate.ts | 21 ++++++++ .../src/messaging/transaction/transfer.ts | 5 +- 10 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 packages/extension-base/src/types/balance/transfer.ts create mode 100644 packages/extension-base/src/types/fee/base.ts create mode 100644 packages/extension-base/src/types/fee/option.ts create mode 100644 packages/extension-base/src/types/fee/subscription.ts create mode 100644 packages/extension-base/src/types/fee/substrate.ts diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 063e4e0cd0..87b47608f8 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -17,6 +17,7 @@ import { CrowdloanContributionsResponse } from '@subwallet/extension-base/servic 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 { RequestSubmitTransfer } 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'; @@ -2146,7 +2147,7 @@ export interface KoniRequestSignatures { // 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]; diff --git a/packages/extension-base/src/services/transaction-service/types.ts b/packages/extension-base/src/services/transaction-service/types.ts index de409489cf..735f04aa2b 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,7 +34,7 @@ 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']; @@ -45,7 +45,7 @@ export interface SWTransactionInput extends SwInputBase, Partial & Partial>; +export type SWTransactionResponse = SwInputBase & Pick & Partial> & TransactionFee; export type ValidateTransactionResponseInput = SWTransactionInput; 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 0000000000..c6898308b7 --- /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; + isXcmTransfer?: boolean; + 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; +} 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 0000000000..306f0e4f1f --- /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'; + +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 0de787635c..e65b99ee0d 100644 --- a/packages/extension-base/src/types/fee/evm.ts +++ b/packages/extension-base/src/types/fee/evm.ts @@ -1,28 +1,33 @@ // 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; } -export interface EvmEIP1995FeeInfo extends BaseFeeInfo { +export interface EvmEIP1559FeeInfo extends BaseFeeInfo { + type: 'evm'; gasPrice: undefined; - maxFeePerGas: BigN; - maxPriorityFeePerGas: BigN; - baseGasFee: BigN; + baseGasFee: string; + options: { + slow: EvmEIP1559FeeOption; + average: EvmEIP1559FeeOption; + fast: EvmEIP1559FeeOption; + default: FeeDefaultOption; + } } -export type EvmFeeInfo = EvmLegacyFeeInfo | EvmEIP1995FeeInfo; +export type EvmFeeInfo = EvmLegacyFeeInfo | EvmEIP1559FeeInfo; export interface EvmLegacyFeeInfoCache extends BaseFeeInfo { gasPrice: string; @@ -31,14 +36,24 @@ export interface EvmLegacyFeeInfoCache extends BaseFeeInfo { baseGasFee: undefined; } -export interface EvmEIP1995FeeInfoCache extends BaseFeeInfo { +export interface EvmEIP1559FeeInfoCache extends BaseFeeInfo { gasPrice: undefined; maxFeePerGas: string; maxPriorityFeePerGas: string; baseGasFee: string; } -export type EvmFeeInfoCache = EvmLegacyFeeInfoCache | EvmEIP1995FeeInfoCache; +export interface EvmLegacyFeeDetail extends EvmLegacyFeeInfo, BaseFeeDetail { + gasLimit: string; +} + +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 +74,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 0bf2854558..0de282e4da 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 0000000000..3970e7a771 --- /dev/null +++ b/packages/extension-base/src/types/fee/option.ts @@ -0,0 +1,12 @@ +// 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; +} 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 0000000000..b7c26d6689 --- /dev/null +++ b/packages/extension-base/src/types/fee/subscription.ts @@ -0,0 +1,20 @@ +// Copyright 2019-2022 @subwallet/extension-base +// SPDX-License-Identifier: Apache-2.0 + +import { FeeChainType } from '@subwallet/extension-base/types'; +import { BehaviorSubject } from 'rxjs'; + +import { EvmEIP1559FeeOption, EvmFeeDetail, EvmFeeInfo } from './evm'; +import { SubstrateFeeDetail, SubstrateFeeInfo, SubstrateTipInfo } from './substrate'; + +export type FeeInfo = EvmFeeInfo | SubstrateFeeInfo; +export type FeeDetail = EvmFeeDetail | SubstrateFeeDetail; +export type FeeCustom = EvmEIP1559FeeOption | SubstrateTipInfo; + +export interface FeeSubscription { + observer: BehaviorSubject; + subscription: Record; + unsubscribe: VoidFunction; +} + +export type GetFeeFunction = (id: string, chain: string, type: FeeChainType) => Promise; 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 0000000000..e84b037aae --- /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-koni-ui/src/messaging/transaction/transfer.ts b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts index 5a431cda98..70d4f7b7f9 100644 --- a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts +++ b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts @@ -4,12 +4,13 @@ import { AmountData, RequestMaxTransferable } from '@subwallet/extension-base/background/KoniTypes'; import { RequestOptimalTransferProcess } from '@subwallet/extension-base/services/balance-service/helpers'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { RequestCrossChainTransfer, RequestTransfer, TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; +import { RequestCrossChainTransfer, TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; +import { RequestSubmitTransfer } 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); } From a5c3591e6ec8009fdfdb73c1969f306a53a1b8b4 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 10 Jan 2025 19:20:42 +0700 Subject: [PATCH 2/9] [Issue-3658] feat: Add utility function and logic to subscribe for get estimate fee --- .../src/services/fee-service/service.ts | 109 ++++++++++- .../src/services/fee-service/utils/index.ts | 180 +++++++----------- packages/extension-base/src/utils/eth.ts | 34 ++++ 3 files changed, 215 insertions(+), 108 deletions(-) diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index 40059860d7..753ad52dce 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,11 @@ export default class FeeService { private evmFeeSubject: BehaviorSubject> = new BehaviorSubject>({}); private useInfura: boolean; + private chainFeeSubscriptionMap: Record> = { + evm: {}, + substrate: {} + }; + constructor (state: KoniState) { this.state = state; this.useInfura = true; @@ -82,4 +87,106 @@ 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(console.error); + } else { + observer.next({ + type: 'substrate', + 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 + }; + } + } + }); + } } 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 d22dccea37..12ca07f0ee 100644 --- a/packages/extension-base/src/services/fee-service/utils/index.ts +++ b/packages/extension-base/src/services/fee-service/utils/index.ts @@ -3,7 +3,7 @@ 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 { 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'; @@ -13,44 +13,63 @@ 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 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) + }, + 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) + }, + 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) + }, + 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); }); - }); + })); + + const [feeInfo, thresholdInfo] = await Promise.all([feeResp.json(), thressholdResp.json()]); + + console.log('feeInfo', feeInfo); + console.log('thresholdInfo', thresholdInfo); + + return parseInfuraFee(feeInfo as InfuraFeeInfo, thresholdInfo as InfuraThresholdInfo); + } catch (e) { + console.warn(e); + + return null; + } }; export const fetchSubWalletFeeData = async (chainId: number, networkKey: string): Promise => { @@ -58,6 +77,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' @@ -106,10 +126,21 @@ 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 +): EvmEIP1559FeeOption => { + // https://www.blocknative.com/blog/eip-1559-fees + const maxFee = baseFee.multipliedBy(2).plus(maxPriorityFee); + + return { maxFeePerGas: maxFee.toFixed(0), maxPriorityFeePerGas: maxPriorityFee.toFixed(0) }; +}; + +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) { @@ -137,11 +168,7 @@ export const calculateGasFeeParams = async (web3: _EvmApi, networkKey: string, u } 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); @@ -166,93 +193,32 @@ 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), + average: getEIP1559GasFee(baseGasFee, averagePriorityFee), + fast: getEIP1559GasFee(baseGasFee, fastPriorityFee), + 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 }; } }; diff --git a/packages/extension-base/src/utils/eth.ts b/packages/extension-base/src/utils/eth.ts index c652b85f5e..52f2d05dec 100644 --- a/packages/extension-base/src/utils/eth.ts +++ b/packages/extension-base/src/utils/eth.ts @@ -1,6 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-base // SPDX-License-Identifier: Apache-2.0 +import { EvmEIP1559FeeOption, EvmFeeInfo, FeeOption } from '@subwallet/extension-base/types'; import BigN from 'bignumber.js'; import BNEther from 'bn.js'; import { ethers } from 'ethers'; @@ -91,3 +92,36 @@ export const signatureToHex = (sig: SignedTransaction): string => { return hexR + hexS + hexV; }; + +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 + }; + } +}; From 582568f97d7940063d6dec458c15a35a9cfb67c1 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 10 Jan 2025 19:21:54 +0700 Subject: [PATCH 3/9] [Issue-3658] feat: Add UI for custom fee transaction --- .../Popup/Transaction/variants/SendFund.tsx | 16 +++++++---- .../FeeEditor/FeeEditorModal.tsx | 27 ++++++++++++------- .../Field/TransactionFee/FeeEditor/index.tsx | 7 ++--- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index 4d6e3c8525..bdde3d81b6 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -14,10 +14,10 @@ import { _isPosChainBridge, _isPosChainL2Bridge } from '@subwallet/extension-bas import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getContractAddressOfToken, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode } from '@subwallet/extension-base/types'; +import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode, FeeOption } from '@subwallet/extension-base/types'; import { CommonStepType } from '@subwallet/extension-base/types/service-base'; import { _reformatAddressWithChain, detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; -import { AccountAddressSelector, AddressInputNew, AddressInputRef, AlertBox, AlertModal, AmountInput, ChainSelector, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; +import { AccountAddressSelector, AddressInputNew, AddressInputRef, AlertBox, AlertModal, AmountInput, ChainSelector, FeeEditor, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; import { useAlert, useDefaultNavigate, useFetchChainAssetInfo, useHandleSubmitMultiTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; @@ -159,6 +159,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const [autoFormatValue] = useLocalStorage(ADDRESS_INPUT_AUTO_FORMAT_VALUE, false); const [maxTransfer, setMaxTransfer] = useState('0'); + const [selectedOption, setSelectedOption] = 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')); @@ -451,12 +452,13 @@ 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, + feeOption: selectedOption }); } else { // Make cross chain transfer @@ -473,7 +475,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone } return sendPromise; - }, []); + }, [selectedOption]); // todo: must refactor later, temporary solution to support SnowBridge const handleBridgeSpendingApproval = useCallback((values: TransferParams): Promise => { @@ -953,6 +955,10 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone + { chainValue !== destChainValue && (
diff --git a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx index ad3a147046..953a07a4e6 100644 --- a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx +++ b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx @@ -1,9 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { FeeDefaultOption, FeeOption } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; import { BasicInputEvent, RadioGroup } from '@subwallet/extension-koni-ui/components'; -import { FeeOption, FeeOptionItem } from '@subwallet/extension-koni-ui/components/Field/TransactionFee/FeeEditor/FeeOptionItem'; +import { FeeOptionItem } from '@subwallet/extension-koni-ui/components/Field/TransactionFee/FeeEditor/FeeOptionItem'; import { FormCallbacks, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { Button, Form, Input, ModalContext, Number, SwModal } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; @@ -15,7 +16,7 @@ import styled from 'styled-components'; type Props = ThemeProps & { modalId: string; - onSelectOption: () => void + onSelectOption: (option: FeeOption) => void }; enum ViewMode { @@ -32,7 +33,7 @@ interface FormProps { customValue: string; } -const OPTIONS: FeeOption[] = [ +const OPTIONS: FeeDefaultOption[] = [ 'slow', 'average', 'fast' @@ -43,7 +44,7 @@ type FeeInfo = { value: number; }; -const feeInfoMap: Record = { +const feeInfoMap: Record = { slow: { time: 60 * 1000 * 30, value: 0.02 @@ -92,14 +93,21 @@ const Component = ({ className, modalId, onSelectOption }: Props): React.ReactEl inactiveModal(modalId); }, [inactiveModal, modalId]); - const _onSelectOption = useCallback(() => { + const _onSelectOption = useCallback((option: FeeOption) => { return () => { - onSelectOption(); + onSelectOption(option); inactiveModal(modalId); }; }, [inactiveModal, modalId, onSelectOption]); - const renderOption = (option: FeeOption) => { + const _onSelectCustomOption = useCallback(() => { + return () => { + onSelectOption('custom'); + inactiveModal(modalId); + }; + }, [inactiveModal, modalId, onSelectOption]); + + const renderOption = (option: FeeDefaultOption) => { return ( @@ -126,7 +134,7 @@ const Component = ({ className, modalId, onSelectOption }: Props): React.ReactEl const onSubmitCustomValue: FormCallbacks['onFinish'] = useCallback(({ customValue }: FormProps) => { inactiveModal(modalId); - onSelectOption(); + onSelectOption('custom'); }, [inactiveModal, modalId, onSelectOption]); return ( @@ -136,6 +144,7 @@ const Component = ({ className, modalId, onSelectOption }: Props): React.ReactEl 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 ed11d81b7b..41b70fd632 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,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { _getAssetDecimals, _getAssetPriceId, _getAssetSymbol } from '@subwallet/extension-base/services/chain-service/utils'; +import { FeeOption } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; @@ -27,7 +28,7 @@ export type RenderFieldNodeParams = { } type Props = ThemeProps & { - onSelect?: () => void; + onSelect?: (option: FeeOption) => void; isLoading?: boolean; tokenSlug: string; renderFieldNode?: (params: RenderFieldNodeParams) => React.ReactNode; @@ -66,8 +67,8 @@ const Component = ({ className, isLoading = false, onSelect, renderFieldNode, to }, 100); }, [activeModal]); - const onSelectOption = useCallback(() => { - onSelect?.(); + const onSelectOption = useCallback((option: FeeOption) => { + onSelect?.(option); }, [onSelect]); const customFieldNode = useMemo(() => { From 5f31ee588c0ac16708588d9c66cc19c5690a91fa Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Mon, 13 Jan 2025 18:22:02 +0700 Subject: [PATCH 4/9] [Issue-3658] feat: Add logic to get maximum transferable amount --- .../src/background/KoniTypes.ts | 3 +- .../src/koni/background/handlers/Extension.ts | 224 ++++++++++++++++++ .../src/services/fee-service/service.ts | 16 ++ .../Popup/Transaction/variants/SendFund.tsx | 103 +++++--- .../src/messaging/transaction/transfer.ts | 6 +- 5 files changed, 314 insertions(+), 38 deletions(-) diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 87b47608f8..9b0b125441 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -17,7 +17,7 @@ import { CrowdloanContributionsResponse } from '@subwallet/extension-base/servic 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 { RequestSubmitTransfer } from '@subwallet/extension-base/types/balance/transfer'; +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'; @@ -2141,6 +2141,7 @@ 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]; diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index f27e77bda9..1b95a2df05 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -1778,6 +1778,227 @@ export default class KoniExtension { } } + private async subscribeMaxTransferable ({ address, chain, destChain, feeCustom, feeOption: _feeOptions, isXcmTransfer, token }: RequestSubscribeTransfer, id: string, port: chrome.runtime.Port): Promise { + const cb = createSubscription<'pri(transfer.subscribe)'>(id, port); + + const tokenInfo = token ? this.#koniState.chainService.getAssetBySlug(token) : this.#koniState.chainService.getNativeTokenInfo(chain); + const chainInfo = this.#koniState.chainService.getChainInfoByKey(chain); + const chainInfoMap = this.#koniState.chainService.getChainInfoMap(); + + console.log('subscribeMaxTransferable', feeCustom, _feeOptions); + + const freeBalanceSubject = new Subject(); + const feeSubject = new Subject(); + const feeChainType: FeeChainType = isXcmTransfer + ? 'substrate' + : _isChainEvmCompatible(chainInfo) && _isTokenTransferredByEvm(tokenInfo) + ? 'evm' + : 'substrate'; + + const convertData = async (freeBalance: AmountData, fee: FeeInfo): Promise => { + const substrateApi = this.#koniState.chainService.getSubstrateApi(chain); + let estimatedFee: string; + let feeOptions: FeeDetail; + let tip = '0'; + let maxTransferable = new BigN(freeBalance.value); + + try { + if (isXcmTransfer) { + const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destChain, tokenInfo.slug); + + if (!destinationTokenInfo) { + const _fee = fee as SubstrateFeeInfo; + + estimatedFee = '0'; + feeOptions = { + ..._fee, + estimatedFee + }; + } else { + maxTransferable = maxTransferable.minus(new BigN(tokenInfo.minAmount || '0').multipliedBy(XCM_MIN_AMOUNT_RATIO)); + const desChainInfo = chainInfoMap[destChain]; + const orgChainInfo = chainInfoMap[chain]; + const recipient = !isEthereumAddress(address) && _isChainEvmCompatible(desChainInfo) && !_isChainEvmCompatible(orgChainInfo) + ? u8aToHex(addressToEvm(address)) + : address + ; + + const mockTx = await createXcmExtrinsic({ + chainInfoMap, + destinationTokenInfo, + originTokenInfo: tokenInfo, + recipient: recipient, + sendingValue: '1000000000000000000', + substrateApi + }); + + try { + const paymentInfo = await mockTx.paymentInfo(address); + + estimatedFee = paymentInfo?.partialFee?.toString() || '0'; + } catch (e) { + estimatedFee = tokenInfo.minAmount || '0'; + } + + const _fee = fee as SubstrateFeeInfo; + + 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; + } + + feeOptions = { + ..._fee, + estimatedFee + }; + } + } else { + if (_isChainEvmCompatible(chainInfo) && _isTokenTransferredByEvm(tokenInfo)) { + const web3 = this.#koniState.chainService.getEvmApi(chain); + let gasLimit: number; + + try { + if (_isNativeToken(tokenInfo)) { + const transaction: TransactionConfig = { + value: 0, + to: '0x0000000000000000000000000000000000000000', // null address + from: address + }; + + gasLimit = await web3.api.eth.estimateGas(transaction); + } else { + const contractAddress = _getContractAddressOfToken(tokenInfo); + const erc20Contract = getERC20Contract(contractAddress, web3); + const address1 = '0xdd718f9Ecaf8f144a3140b79361b5D713D3A6b19'; + const address2 = '0x5e10e440FEce4dB0b16a6159A4536efb74d32E9b'; + const to = address1.toLowerCase() === address.toLowerCase() ? address2 : address1; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + gasLimit = await erc20Contract.methods.transfer(to, freeBalance.value).estimateGas({ from: address }) as number; + } + } catch (e) { + console.error(e); + gasLimit = 0; + } + + const _fee = fee as EvmFeeInfo; + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const combineFee = combineEthFee(_fee, _feeOptions, _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 { + const [mockTx] = await createTransferExtrinsic({ + from: address, + networkKey: chain, + substrateApi, + to: address, + tokenInfo, + transferAll: true, + value: '1000000000000000000' + }); + + const paymentInfo = await mockTx?.paymentInfo(address); + const _fee = fee as SubstrateFeeInfo; + + 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; + } + + estimatedFee = paymentInfo?.partialFee?.toString() || '0'; + feeOptions = { + ..._fee, + estimatedFee + }; + } + } + } catch (e) { + estimatedFee = '0'; + + if (fee.type === 'substrate') { + feeOptions = { + ...fee, + estimatedFee + }; + } else { + feeOptions = { + ...fee, + estimatedFee, + gasLimit: '0' + }; + } + + console.warn('Unable to estimate fee', e); + } + + maxTransferable = maxTransferable + .minus(new BigN(estimatedFee).multipliedBy(isXcmTransfer ? XCM_FEE_RATIO : 1)) + .minus(tip); + + return { + maxTransferable: !_isNativeToken(tokenInfo) + ? freeBalance.value + : maxTransferable.gt(BN_ZERO) ? (maxTransferable.toFixed(0) || '0') : '0', + feeOptions: feeOptions, + feeType: feeChainType, + id: id + }; + }; + + const subscription = combineLatest({ + freeBalance: freeBalanceSubject, + fee: feeSubject + }) + .subscribe({ + next: ({ fee, freeBalance }) => { + convertData(freeBalance, fee) + .then(cb) + .catch(console.error); + } + }); + + const [unsubBalance, freeBalance] = await this.#koniState.balanceService.subscribeBalance(address, chain, token, 'transferable', undefined, (data) => { + freeBalanceSubject.next(data); // Must be called after subscription + }); + + const fee = await this.#koniState.feeService.subscribeChainFee(id, chain, feeChainType, (data) => { + feeSubject.next(data); // Must be called after subscription + }); + + const unsub = () => { + subscription.unsubscribe(); + unsubBalance(); + this.#koniState.feeService.unsubscribeChainFee(id, chain, feeChainType); + }; + + this.createUnsubscriptionHandle( + id, + unsub + ); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return convertData(freeBalance, fee); + } + private async getXcmMaxTransferable (originTokenInfo: _ChainAsset, destChain: string, address: string): Promise { const substrateApi = this.#koniState.chainService.getSubstrateApi(originTokenInfo.originChain); const chainInfoMap = this.#koniState.chainService.getChainInfoMap(); @@ -4274,6 +4495,9 @@ export default class KoniExtension { 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)': diff --git a/packages/extension-base/src/services/fee-service/service.ts b/packages/extension-base/src/services/fee-service/service.ts index 753ad52dce..07c92cf9b2 100644 --- a/packages/extension-base/src/services/fee-service/service.ts +++ b/packages/extension-base/src/services/fee-service/service.ts @@ -189,4 +189,20 @@ export default class FeeService { } }); } + + 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-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index bdde3d81b6..715a30f241 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -15,19 +15,20 @@ import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode, FeeOption } from '@subwallet/extension-base/types'; +import { ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { CommonStepType } from '@subwallet/extension-base/types/service-base'; import { _reformatAddressWithChain, detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; import { AccountAddressSelector, AddressInputNew, AddressInputRef, AlertBox, AlertModal, AmountInput, ChainSelector, FeeEditor, HiddenInput, TokenItemType, TokenSelector } from '@subwallet/extension-koni-ui/components'; import { ADDRESS_INPUT_AUTO_FORMAT_VALUE } from '@subwallet/extension-koni-ui/constants'; import { MktCampaignModalContext } from '@subwallet/extension-koni-ui/contexts/MktCampaignModalContext'; -import { useAlert, useDefaultNavigate, useFetchChainAssetInfo, useHandleSubmitMultiTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; +import { useAlert, useDefaultNavigate, useFetchChainAssetInfo, useGetNativeTokenBasicInfo, useHandleSubmitMultiTransaction, useNotification, usePreCheckAction, useRestoreTransaction, useSelector, useSetCurrentPage, useTransactionContext, useWatchTransaction } from '@subwallet/extension-koni-ui/hooks'; import useGetConfirmationByScreen from '@subwallet/extension-koni-ui/hooks/campaign/useGetConfirmationByScreen'; -import { approveSpending, getMaxTransfer, getOptimalTransferProcess, isTonBounceableAddress, makeCrossChainTransfer, makeTransfer } from '@subwallet/extension-koni-ui/messaging'; +import { approveSpending, cancelSubscription, getOptimalTransferProcess, isTonBounceableAddress, makeCrossChainTransfer, makeTransfer, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; import { CommonActionType, commonProcessReducer, DEFAULT_COMMON_PROCESS } from '@subwallet/extension-koni-ui/reducer'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { AccountAddressItemType, ChainItemType, FormCallbacks, Theme, ThemeProps, TransferParams } from '@subwallet/extension-koni-ui/types'; import { findAccountByAddress, formatBalance, getChainsByAccountAll, getChainsByAccountType, getReformatedAddressRelatedToChain, noop } from '@subwallet/extension-koni-ui/utils'; -import { Button, Form, Icon } from '@subwallet/react-ui'; +import { Button, Form, Icon, Number } from '@subwallet/react-ui'; import { Rule } from '@subwallet/react-ui/es/form'; import BigN from 'bignumber.js'; import CN from 'classnames'; @@ -158,10 +159,10 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); const [autoFormatValue] = useLocalStorage(ADDRESS_INPUT_AUTO_FORMAT_VALUE, false); - const [maxTransfer, setMaxTransfer] = useState('0'); const [selectedOption, setSelectedOption] = 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')); + const nativeTokenBasicInfo = useGetNativeTokenBasicInfo(chainValue); const currentConfirmation = useMemo(() => { if (chainValue && destChainValue) { @@ -196,10 +197,14 @@ 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 estimatedFee = useMemo((): string => transferInfo?.feeOptions.estimatedFee || '0', [transferInfo]); + + console.log('transferInfo?.feeOptions', transferInfo?.feeOptions); const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); @@ -331,6 +336,10 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone }, [accounts, autoFormatValue, chainInfoMap, form, ledgerGenericAllowNetworks]); const validateAmount = useCallback((rule: Rule, amount: string): Promise => { + const maxTransfer = transferInfo?.maxTransferable || '0'; + + console.log('maxTransfer', maxTransfer); + if (!amount) { return Promise.reject(t('Amount is required')); } @@ -350,7 +359,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) => { @@ -552,12 +561,12 @@ 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 onSubmit: FormCallbacks['onFinish'] = useCallback((values: TransferParams) => { const options: TransferOptions = { @@ -756,52 +765,68 @@ 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: selectedOption + }, callback) + .then((callback)) + .catch((e) => { + console.error(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, selectedOption]); 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({ @@ -947,7 +972,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')} @@ -956,6 +981,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone @@ -978,12 +1004,17 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone /> ) } +
-
- {t('Fee paid in')} +
{t('Fee paid in')}
+
+ +
{symbol}
@@ -237,7 +247,20 @@ 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 }, '.__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 d78ddad317..85295d3d75 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 6798a70cb9..0d9f277b8a 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 @@ -4,6 +4,7 @@ import { _getAssetDecimals, _getAssetPriceId, _getAssetSymbol } from '@subwallet/extension-base/services/chain-service/utils'; import { FeeDetail, FeeOption } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; +import { BN_TEN } 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'; @@ -31,14 +32,15 @@ type Props = ThemeProps & { onSelect?: (option: FeeOption) => void; isLoading?: boolean; tokenSlug: string; - feeOptions?: FeeDetail; + feeOptionsInfo?: FeeDetail; + estimateFee: string; renderFieldNode?: (params: RenderFieldNodeParams) => React.ReactNode; }; // todo: will update dynamic later const modalId = 'FeeEditorModalId'; -const Component = ({ className, feeOptions, isLoading = false, onSelect, renderFieldNode, tokenSlug }: Props): React.ReactElement => { +const Component = ({ className, estimateFee, feeOptionsInfo, isLoading = false, onSelect, renderFieldNode, tokenSlug }: Props): React.ReactElement => { const { t } = useTranslation(); const { activeModal } = useContext(ModalContext); const assetRegistry = useSelector((root) => root.assetRegistry.assetRegistry); @@ -52,6 +54,7 @@ const Component = ({ className, feeOptions, isLoading = false, onSelect, renderF const decimals = _getAssetDecimals(tokenAsset); // @ts-ignore const priceId = _getAssetPriceId(tokenAsset); + const priceValue = priceMap[priceId] || 0; const symbol = _getAssetSymbol(tokenAsset); const feeValue = useMemo(() => { @@ -62,6 +65,13 @@ const Component = ({ className, feeOptions, isLoading = false, onSelect, renderF return BN_ZERO; }, []); + const convertedFeeValue = useMemo(() => { + return new BigN(estimateFee) + .dividedBy(BN_TEN.pow(decimals || 0)) + .multipliedBy(priceValue) + .toNumber(); + }, [decimals, estimateFee, priceValue]); + const onClickEdit = useCallback(() => { setTimeout(() => { activeModal(modalId); @@ -104,7 +114,7 @@ const Component = ({ className, feeOptions, isLoading = false, onSelect, renderF className={'__fee-value'} decimal={decimals} suffix={symbol} - value={feeValue} + value={estimateFee} />
@@ -116,7 +126,7 @@ const Component = ({ className, feeOptions, isLoading = false, onSelect, renderF className={'__fee-price-value'} decimal={0} prefix={'~ $'} - value={feePriceValue} + value={convertedFeeValue} /> ); From fccd2b160fb506034264238080157d2c1ca52bf6 Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Thu, 16 Jan 2025 20:06:24 +0700 Subject: [PATCH 8/9] [Issue-3658] Update custom fee for XCM, Bridge, dapps --- .../src/core/logic-validation/request.ts | 34 +++-- .../src/koni/background/handlers/Extension.ts | 127 +++++++++++++----- .../transfer/smart-contract.ts | 121 ++++++++++------- .../transfer/xcm/availBridge.ts | 29 ++-- .../balance-service/transfer/xcm/index.ts | 33 +++-- .../transfer/xcm/polygonBridge.ts | 42 +++--- .../balance-service/transfer/xcm/posBridge.ts | 39 +++--- .../transfer/xcm/snowBridge.ts | 20 +-- .../src/services/fee-service/utils/index.ts | 27 ++-- .../src/services/transaction-service/index.ts | 23 +++- packages/extension-base/src/types/fee/evm.ts | 4 +- .../src/types/transaction/request.ts | 6 +- .../Popup/Transaction/variants/SendFund.tsx | 3 +- 13 files changed, 326 insertions(+), 182 deletions(-) diff --git a/packages/extension-base/src/core/logic-validation/request.ts b/packages/extension-base/src/core/logic-validation/request.ts index a88911f93a..cf39c2e452 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,24 @@ 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 priority = await calculateGasFeeParams(evmApi, networkKey || ''); + 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/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 31a637eb6e..de347ce9b9 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -11,12 +11,12 @@ import { createSubscription } from '@subwallet/extension-base/background/handler import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; -import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; +import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO, XCM_MIN_AMOUNT_RATIO } 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'; import { ALLOWED_PATH } from '@subwallet/extension-base/defaults'; -import { getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; +import { getERC20Contract, getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; import { _ERC721_ABI, isAvailBridgeGatewayContract, isSnowBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { resolveAzeroAddressToDomain, resolveAzeroDomainToAddress } from '@subwallet/extension-base/koni/api/dotsama/domain'; import { parseSubstrateTransaction } from '@subwallet/extension-base/koni/api/dotsama/parseTransaction'; @@ -49,14 +49,16 @@ import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectN 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, EvmEIP1559FeeOption, EvmFeeInfo, FeeChainType, FeeDetail, FeeInfo, GetFeeFunction, 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, 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, SubstrateFeeInfo, 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, BN_ZERO, combineAllAccountProxy, combineEthFee, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, toBNString, 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 { metadataExpand } from '@subwallet/extension-chains'; import { MetadataDef } from '@subwallet/extension-inject/types'; import { getKeypairTypeByAddress, isAddress, isSubstrateAddress, isTonAddress } from '@subwallet/keyring'; @@ -77,7 +79,7 @@ import { Metadata, TypeRegistry } from '@polkadot/types'; import { ChainProperties } from '@polkadot/types/interfaces'; import { AnyJson, Registry, SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import { assert, hexStripPrefix, hexToU8a, isAscii, isHex, u8aToHex } from '@polkadot/util'; -import { decodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; +import { addressToEvm, decodeAddress, isEthereumAddress } from '@polkadot/util-crypto'; export function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON { return (value as SignerPayloadJSON).genesisHash !== undefined; @@ -1307,15 +1309,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, to, tokenSlug, transferAll, transferBounceable, value } = inputData; const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(tokenSlug); 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 +1327,17 @@ 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 getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; // todo: refactor: merge getERC20TransactionObject & getEVMTransactionObject // Estimate with EVM API @@ -1339,37 +1345,57 @@ 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, + feeOption, + from, + getChainFee, + to, + transferAll, + value: txVal + }); } else { [ transaction, transferAmount.value - ] = await getEVMTransactionObject(chainInfo, from, to, txVal, !!transferAll, evmApi); + ] = await getEVMTransactionObject( + { chain, + evmApi, + feeCustom, + feeOption, + from, + getChainFee, + 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 @@ -1398,20 +1424,20 @@ export default class KoniExtension { // 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 +1460,9 @@ export default class KoniExtension { errors, warnings, address: from, - chain: networkKey, + chain, + feeCustom, + feeOption, chainType, transferNativeAmount, transaction, @@ -1448,7 +1476,7 @@ export default class KoniExtension { } private async makeCrossChainTransfer (inputData: RequestCrossChainTransfer): Promise { - const { destinationNetworkKey, from, originNetworkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; + const { destinationNetworkKey, feeCustom, feeOption, from, originNetworkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; const originTokenInfo = this.#koniState.getAssetBySlug(tokenSlug); const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destinationNetworkKey, tokenSlug); @@ -1469,9 +1497,14 @@ export default class KoniExtension { let additionalValidator: undefined | ((inputTransaction: SWTransactionResponse) => Promise); let eventsHandler: undefined | ((eventEmitter: TransactionEmitter) => void); + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + if (fromKeyPair && destinationTokenInfo) { const evmApi = this.#koniState.getEvmApi(originNetworkKey); const substrateApi = this.#koniState.getSubstrateApi(originNetworkKey); + const params: CreateXcmExtrinsicProps = { destinationTokenInfo, originTokenInfo, @@ -1480,7 +1513,10 @@ export default class KoniExtension { recipient: to, chainInfoMap, substrateApi, - evmApi + evmApi, + getChainFee, + feeCustom, + feeOption }; let funcCreateExtrinsic: FunctionCreateXcmExtrinsic; @@ -1578,7 +1614,11 @@ export default class KoniExtension { }); } - const transaction = await getERC721Transaction(this.#koniState.getEvmApi(networkKey), networkKey, contractAddress, senderAddress, recipientAddress, tokenId); + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + + const transaction = await getERC721Transaction(this.#koniState.getEvmApi(networkKey), networkKey, contractAddress, senderAddress, recipientAddress, tokenId, getChainFee); // this.addContact(recipientAddress); @@ -1785,8 +1825,6 @@ export default class KoniExtension { const chainInfo = this.#koniState.chainService.getChainInfoByKey(chain); const chainInfoMap = this.#koniState.chainService.getChainInfoMap(); - console.log('subscribeMaxTransferable', feeCustom, _feeOptions); - const freeBalanceSubject = new Subject(); const feeSubject = new Subject(); const feeChainType: FeeChainType = isXcmTransfer @@ -1823,13 +1861,18 @@ export default class KoniExtension { : address ; + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + const mockTx = await createXcmExtrinsic({ chainInfoMap, destinationTokenInfo, originTokenInfo: tokenInfo, recipient: recipient, sendingValue: '1000000000000000000', - substrateApi + substrateApi, + getChainFee }); try { @@ -2011,9 +2054,13 @@ export default class KoniExtension { 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))); + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + if (destinationTokenInfo) { const [bnMockExecutionFee, { value }] = await Promise.all([ - getXcmMockTxFee(substrateApi, chainInfoMap, originTokenInfo, destinationTokenInfo), + getXcmMockTxFee(substrateApi, chainInfoMap, originTokenInfo, destinationTokenInfo, getChainFee), this.getAddressTransferableBalance({ extrinsicType: ExtrinsicType.TRANSFER_XCM, address, networkKey: originTokenInfo.originChain, token: originTokenInfo.slug }) ]); @@ -4100,7 +4147,14 @@ export default class KoniExtension { } else { const evmApi = this.#koniState.getEvmApi(chain); - transaction = await getClaimTxOnEthereum(chain, notification, evmApi); + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + + const id = getId(); + const feeInfo = await getChainFee(id, chain, 'evm') as EvmFeeInfo; + + transaction = await getClaimTxOnEthereum(chain, notification, evmApi, feeInfo); chainType = ChainType.EVM; } @@ -4131,10 +4185,17 @@ export default class KoniExtension { const evmApi = this.#koniState.getEvmApi(chain); const metadata = notification.metadata as ClaimPolygonBridgeNotificationMetadata; + const getChainFee: GetFeeFunction = (id, chain, type) => { + return this.#koniState.feeService.subscribeChainFee(id, chain, type); + }; + + const id = getId(); + const feeInfo = await getChainFee(id, 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; @@ -4517,7 +4578,7 @@ 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)': 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 c53d389292..7c75e3e65c 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,68 @@ // 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, GetFeeFunction, TransactionFee } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; 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; + getChainFee: GetFeeFunction; + to: string; + transferAll: boolean; + value: string; + evmApi: _EvmApi; +} - const priority = await calculateGasFeeParams(web3Api, networkKey); +export async function getEVMTransactionObject ({ chain, + evmApi, + feeCustom: _feeCustom, + feeOption, + from, + getChainFee, + to, + transferAll, + value }: TransferEvmProps): Promise<[TransactionConfig, string]> { + const id = getId(); + const feeCustom = _feeCustom as EvmEIP1559FeeOption; + const feeInfo = await getChainFee(id, chain, 'evm') as EvmFeeInfo; + + 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 +72,20 @@ export async function getEVMTransactionObject ( } export async function getERC20TransactionObject ( - assetAddress: string, - chainInfo: _ChainInfo, - from: string, - to: string, - value: string, - transferAll: boolean, - evmApi: _EvmApi + { assetAddress, + chain, + evmApi, + feeCustom: _feeCustom, + feeOption, + from, + getChainFee, + 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; @@ -87,25 +102,25 @@ export async function getERC20TransactionObject ( return erc20Contract.methods.transfer(to, transferValue).encodeABI() as string; } + const id = getId(); + const transferData = generateTransferData(to, transferValue); - const [gasLimit, priority] = await Promise.all([ + const [gasLimit, _feeInfo] = 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) + erc20Contract.methods.transfer(to, transferValue).estimateGas({ from }) as number, + getChainFee(id, chain, 'evm') ]); + const feeInfo = _feeInfo as EvmFeeInfo; + const feeCombine = combineEthFee(feeInfo, feeOption, feeCustom); + const transactionObject = { gas: gasLimit, from, value: '0', to: assetAddress, data: transferData, - gasPrice: priority.gasPrice, - maxFeePerGas: priority.maxFeePerGas?.toString(), - maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + ...feeCombine } as TransactionConfig; if (transferAll) { @@ -116,24 +131,34 @@ export async function getERC20TransactionObject ( return [transactionObject, transferValue]; } +interface TransferERC20Props extends TransactionFee { + assetAddress: string; + chain: string; + evmApi: _EvmApi; + from: string; + getChainFee: GetFeeFunction; + 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, + getChainFee: GetFeeFunction): 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 +170,18 @@ export async function getERC721Transaction ( throw error; } + const id = getId(); + const feeInfo = await getChainFee(id, chain, 'evm') 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/xcm/availBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts index 1253eac496..5cef7c48c2 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,11 @@ 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, FeeOption, GetFeeFunction } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { decodeAddress } from '@subwallet/keyring'; import { PrefixedHexString } from 'ethereumjs-util'; import { TransactionConfig } from 'web3-core'; @@ -54,7 +56,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, getChainFee: GetFeeFunction, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const availBridgeContractAddress = getAvailBridgeGatewayContract(originChainInfo.slug); const ABI = getAvailBridgeAbi(originChainInfo.slug); const availBridgeContract = getWeb3Contract(availBridgeContractAddress, evmApi, ABI); @@ -62,18 +64,20 @@ 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 id = getId(); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = await getChainFee(id, originChainInfo.slug, 'evm') 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 +172,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 +218,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 6f6407d265..3070e61914 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 @@ -12,6 +12,7 @@ import { getExtrinsicByXtokensPallet } from '@subwallet/extension-base/services/ 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 { GetFeeFunction, TransactionFee } from '@subwallet/extension-base/types'; import BigN from 'bignumber.js'; import { TransactionConfig } from 'web3-core'; @@ -30,17 +31,18 @@ export type CreateXcmExtrinsicProps = { substrateApi?: _SubstrateApi; chainInfoMap: Record; sender?: string; -} + getChainFee: GetFeeFunction; +} & TransactionFee; export type FunctionCreateXcmExtrinsic = (props: CreateXcmExtrinsicProps) => Promise | TransactionConfig>; export const createSnowBridgeExtrinsic = async ({ chainInfoMap, destinationTokenInfo, evmApi, - originTokenInfo, - recipient, - sender, - sendingValue }: CreateXcmExtrinsicProps): Promise => { + feeCustom, + feeOption, + getChainFee, + originTokenInfo, recipient, sender, sendingValue }: CreateXcmExtrinsicProps): Promise => { const originChainInfo = chainInfoMap[originTokenInfo.originChain]; const destinationChainInfo = chainInfoMap[destinationTokenInfo.originChain]; @@ -56,11 +58,12 @@ export const createSnowBridgeExtrinsic = async ({ chainInfoMap, throw Error('Sender is required'); } - return getSnowBridgeEvmTransfer(originTokenInfo, originChainInfo, destinationChainInfo, sender, recipient, sendingValue, evmApi); + return getSnowBridgeEvmTransfer(originTokenInfo, originChainInfo, destinationChainInfo, sender, recipient, sendingValue, evmApi, getChainFee, feeCustom, feeOption); }; export const createXcmExtrinsic = async ({ chainInfoMap, destinationTokenInfo, + getChainFee, originTokenInfo, recipient, sendingValue, @@ -90,9 +93,9 @@ export const createXcmExtrinsic = async ({ chainInfoMap, export const createAvailBridgeTxFromEth = ({ chainInfoMap, evmApi, - originTokenInfo, - recipient, - sender, + feeCustom, + feeOption, + getChainFee, originTokenInfo, recipient, sender, sendingValue }: CreateXcmExtrinsicProps): Promise => { const originChainInfo = chainInfoMap[originTokenInfo.originChain]; @@ -104,7 +107,7 @@ export const createAvailBridgeTxFromEth = ({ chainInfoMap, throw Error('Sender is required'); } - return getAvailBridgeTxFromEth(originChainInfo, sender, recipient, sendingValue, evmApi); + return getAvailBridgeTxFromEth(originChainInfo, sender, recipient, sendingValue, evmApi, getChainFee, feeCustom, feeOption); }; export const createAvailBridgeExtrinsicFromAvail = async ({ recipient, sendingValue, substrateApi }: CreateXcmExtrinsicProps): Promise> => { @@ -118,6 +121,9 @@ export const createAvailBridgeExtrinsicFromAvail = async ({ recipient, sendingVa export const createPolygonBridgeExtrinsic = async ({ chainInfoMap, destinationTokenInfo, evmApi, + feeCustom, + feeOption, + getChainFee, originTokenInfo, recipient, sender, @@ -150,10 +156,10 @@ export const createPolygonBridgeExtrinsic = async ({ chainInfoMap, ? _createPosBridgeL2toL1Extrinsic : _createPosBridgeL1toL2Extrinsic; - return createExtrinsic(originTokenInfo, originChainInfo, sender, recipient, sendingValue, evmApi); + return createExtrinsic(originTokenInfo, originChainInfo, sender, recipient, sendingValue, evmApi, getChainFee, feeCustom, feeOption); }; -export const getXcmMockTxFee = async (substrateApi: _SubstrateApi, chainInfoMap: Record, originTokenInfo: _ChainAsset, destinationTokenInfo: _ChainAsset): Promise => { +export const getXcmMockTxFee = async (substrateApi: _SubstrateApi, chainInfoMap: Record, originTokenInfo: _ChainAsset, destinationTokenInfo: _ChainAsset, getChainFee: GetFeeFunction): Promise => { try { const destChainInfo = chainInfoMap[destinationTokenInfo.originChain]; const originChainInfo = chainInfoMap[originTokenInfo.originChain]; @@ -172,7 +178,8 @@ export const getXcmMockTxFee = async (substrateApi: _SubstrateApi, chainInfoMap: sender, recipient, sendingValue: '1000000000000000000', - substrateApi + substrateApi, + getChainFee }); const paymentInfo = await mockTx.paymentInfo(fakeAddress); 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 e8d2fc5f92..11cd0985d0 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,10 @@ 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 { EvmFeeInfo, FeeCustom, FeeOption, GetFeeFunction } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { TransactionConfig } from 'web3-core'; import { ContractSendMethod } from 'web3-eth-contract'; @@ -39,7 +41,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, + getChainFee: GetFeeFunction, + 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 +71,17 @@ async function createPolygonBridgeTransaction (tokenInfo: _ChainAsset, originCha '0x' ); const transferEncodedCall = transferCall.encodeABI(); - const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + const id = getId(); + const feeInfo = await getChainFee(id, originChainInfo.slug, 'evm') as EvmFeeInfo; + + const feeCombine = combineEthFee(feeInfo); 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 +91,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, getChainFee: GetFeeFunction, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { + return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 1, evmApi, getChainFee, 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, getChainFee: GetFeeFunction, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { + return createPolygonBridgeTransaction(tokenInfo, originChainInfo, sender, recipientAddress, value, 0, evmApi, getChainFee, 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 +115,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 4b64c45437..942a4b943e 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,11 @@ 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, FeeOption, GetFeeFunction } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { TransactionConfig } from 'web3-core'; import { ContractSendMethod } from 'web3-eth-contract'; @@ -32,23 +33,27 @@ 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, getChainFee: GetFeeFunction, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const posBridgeContractAddress = getPosL1BridgeContract(originChainInfo.slug); const posBridgeContract = getWeb3Contract(posBridgeContractAddress, evmApi, _POS_BRIDGE_ABI); + const id = getId(); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = await getChainFee(id, originChainInfo.slug, 'evm') 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 +63,25 @@ 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, getChainFee: GetFeeFunction, 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 id = getId(); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = await getChainFee(id, originChainInfo.slug, 'evm') 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 +91,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 +127,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 7234006364..76b3f98e2f 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,9 @@ 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, FeeOption, GetFeeFunction } from '@subwallet/extension-base/types'; +import { combineEthFee } from '@subwallet/extension-base/utils'; +import { getId } from '@subwallet/extension-base/utils/getId'; import { TransactionConfig } from 'web3-core'; import { Contract } from 'web3-eth-contract'; @@ -22,7 +24,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, getChainFee: GetFeeFunction, feeCustom?: FeeCustom, feeOption?: FeeOption): Promise { const snowBridgeContractAddress = getSnowBridgeGatewayContract(originChainInfo.slug); const snowBridgeContract = getWeb3Contract(snowBridgeContractAddress, evmApi, _SNOWBRIDGE_GATEWAY_ABI); const tokenContract = _getContractAddressOfToken(tokenInfo); @@ -38,19 +40,19 @@ 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 id = getId(); + const _feeCustom = feeCustom as EvmEIP1559FeeOption; + const feeInfo = await getChainFee(id, originChainInfo.slug, 'evm') as EvmFeeInfo; + 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/fee-service/utils/index.ts b/packages/extension-base/src/services/fee-service/utils/index.ts index 12ca07f0ee..09b3a7a82f 100644 --- a/packages/extension-base/src/services/fee-service/utils/index.ts +++ b/packages/extension-base/src/services/fee-service/utils/index.ts @@ -26,15 +26,21 @@ export const parseInfuraFee = (info: InfuraFeeInfo, threshold: InfuraThresholdIn 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) + 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) + 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) + 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' } @@ -59,12 +65,11 @@ export const fetchInfuraFeeData = async (chainId: number, infuraAuth?: string): }); })); - const [feeInfo, thresholdInfo] = await Promise.all([feeResp.json(), thressholdResp.json()]); + const [feeInfo, thresholdInfo]: [InfuraFeeInfo, InfuraThresholdInfo] = await Promise.all([ + feeResp.json(), + thressholdResp.json()]); - console.log('feeInfo', feeInfo); - console.log('thresholdInfo', thresholdInfo); - - return parseInfuraFee(feeInfo as InfuraFeeInfo, thresholdInfo as InfuraThresholdInfo); + return parseInfuraFee(feeInfo, thresholdInfo); } catch (e) { console.warn(e); @@ -159,11 +164,11 @@ 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 }; } diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index 2249f18118..32103fb8dd 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'; @@ -98,7 +100,7 @@ export default class TransactionService { errors: transactionInput.errors || [], warnings: transactionInput.warnings || [] }; - const { additionalValidator, address, chain, extrinsicType } = validationResponse; + const { additionalValidator, address, chain, extrinsicType, feeCustom, feeOption } = validationResponse; const chainInfo = this.state.chainService.getChainInfoByKey(chain); const blockedConfigObjects = await fetchBlockedConfigObjects(); @@ -138,7 +140,10 @@ 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, feeInfo); const chainInfoMap = this.state.chainService.getChainInfoMap(); @@ -924,6 +929,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 +1111,8 @@ export default class TransactionService { return emitter; } - private signAndSendSubstrateTransaction ({ address, chain, id, transaction, url }: SWTransaction): TransactionEmitter { + private signAndSendSubstrateTransaction ({ address, chain, feeCustom, id, transaction, url }: SWTransaction): TransactionEmitter { + const tip = (feeCustom as SubstrateTipInfo)?.tip || '0'; const emitter = new EventEmitter(); const eventData: TransactionEventResponse = { id, @@ -1123,6 +1137,7 @@ export default class TransactionService { } as SignerResult; } } as Signer, + tip, withSignedTransaction: true }; diff --git a/packages/extension-base/src/types/fee/evm.ts b/packages/extension-base/src/types/fee/evm.ts index 8fe7c10f91..5b3d9960cf 100644 --- a/packages/extension-base/src/types/fee/evm.ts +++ b/packages/extension-base/src/types/fee/evm.ts @@ -13,8 +13,8 @@ export interface EvmLegacyFeeInfo extends BaseFeeInfo { export interface EvmEIP1559FeeOption { maxFeePerGas: string; maxPriorityFeePerGas: string; - minWaitTimeEstimate: number; - maxWaitTimeEstimate: number; + minWaitTimeEstimate?: number; + maxWaitTimeEstimate?: number; } export enum FeeOptionKey { diff --git a/packages/extension-base/src/types/transaction/request.ts b/packages/extension-base/src/types/transaction/request.ts index 3f59aec546..9e6dcbc9bf 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[]; @@ -18,13 +18,13 @@ export interface RequestBaseTransfer { transferBounceable?: 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, diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index 8f197af4ac..18ba309897 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -474,7 +474,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone to, value, transferAll: options.isTransferAll, - transferBounceable: options.isTransferBounceable + transferBounceable: options.isTransferBounceable, + feeOption: selectedOption }); } From 426ecb4d8f8b351a1abfafa0b4359f573509e09d Mon Sep 17 00:00:00 2001 From: Frenkie Nguyen Date: Fri, 17 Jan 2025 13:31:10 +0700 Subject: [PATCH 9/9] [Issue-3658] tmp --- .../src/core/logic-validation/request.ts | 1 - .../src/koni/background/handlers/Extension.ts | 10 +++--- .../Popup/Transaction/variants/SendFund.tsx | 21 ++++++++----- .../FeeEditor/FeeEditorModal.tsx | 31 ++++++++----------- .../Field/TransactionFee/FeeEditor/index.tsx | 10 +++--- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/extension-base/src/core/logic-validation/request.ts b/packages/extension-base/src/core/logic-validation/request.ts index cf39c2e452..bc34b70ada 100644 --- a/packages/extension-base/src/core/logic-validation/request.ts +++ b/packages/extension-base/src/core/logic-validation/request.ts @@ -434,7 +434,6 @@ 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 || ''); 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; diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index ab797364d4..1e01202cbe 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -11,12 +11,12 @@ import { createSubscription } from '@subwallet/extension-base/background/handler import { AccountExternalError, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CronReloadRequest, CrowdloanJson, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, MetadataItem, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangePriceCurrency, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestConfirmationComplete, RequestConfirmationCompleteTon, RequestConnectWalletConnect, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetTransaction, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveAppConfig, RequestSaveBrowserConfig, RequestSaveOSConfig, RequestSaveRecentAccount, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseChangeMasterPassword, ResponseFindRawMetadata, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseNftImport, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSubscribeHistory, ResponseUnlockKeyring, ShowCampaignPopupRequest, StakingJson, StakingRewardJson, StakingType, SufficientMetadata, ThemeNames, TransactionHistoryItem, TransactionResponse, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountAuthType, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountExport, RequestAuthorizeCancel, RequestAuthorizeReject, RequestCurrentAccountAddress, RequestMetadataApprove, RequestMetadataReject, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; -import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO } from '@subwallet/extension-base/constants'; +import { ALL_ACCOUNT_KEY, LATEST_SESSION, XCM_FEE_RATIO, XCM_MIN_AMOUNT_RATIO } 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'; import { ALLOWED_PATH } from '@subwallet/extension-base/defaults'; -import { getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; +import { getERC20Contract, getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; import { _ERC721_ABI, isAvailBridgeGatewayContract, isSnowBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { resolveAzeroAddressToDomain, resolveAzeroDomainToAddress } from '@subwallet/extension-base/koni/api/dotsama/domain'; import { parseSubstrateTransaction } from '@subwallet/extension-base/koni/api/dotsama/parseTransaction'; @@ -49,15 +49,16 @@ import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectN 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, EvmEIP1559FeeOption, EvmFeeInfo, FeeChainType, FeeDetail, FeeInfo, GetFeeFunction, 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, 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, SubstrateFeeInfo, 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, BN_ZERO, combineAllAccountProxy, combineEthFee, createTransactionFromRLP, isSameAddress, MODULE_SUPPORT, reformatAddress, signatureToHex, toBNString, 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'; @@ -1309,6 +1310,7 @@ export default class KoniExtension { } private async makeTransfer (inputData: RequestSubmitTransfer): Promise { + console.log('inputData', inputData); const { chain, feeCustom, feeOption, from, to, tokenSlug, transferAll, transferBounceable, value } = inputData; const transferTokenInfo = this.#koniState.chainService.getAssetBySlug(tokenSlug); const [errors, ,] = validateTransferRequest(transferTokenInfo, from, to, value, transferAll); diff --git a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx index 18ba309897..4f283ccbbc 100644 --- a/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx +++ b/packages/extension-koni-ui/src/Popup/Transaction/variants/SendFund.tsx @@ -14,7 +14,7 @@ import { _isPosChainBridge, _isPosChainL2Bridge } from '@subwallet/extension-bas import { _getAssetDecimals, _getAssetName, _getAssetOriginChain, _getAssetSymbol, _getContractAddressOfToken, _getMultiChainAsset, _getOriginChainOfAsset, _getTokenMinAmount, _isChainEvmCompatible, _isNativeToken, _isTokenTransferredByEvm } from '@subwallet/extension-base/services/chain-service/utils'; import { TON_CHAINS } from '@subwallet/extension-base/services/earning-service/constants'; import { SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode, FeeOption } from '@subwallet/extension-base/types'; +import { AccountChainType, AccountProxy, AccountProxyType, AccountSignMode, AnalyzedGroup, BasicTxWarningCode, TransactionFee } from '@subwallet/extension-base/types'; import { ResponseSubscribeTransfer } from '@subwallet/extension-base/types/balance/transfer'; import { CommonStepType } from '@subwallet/extension-base/types/service-base'; import { _reformatAddressWithChain, detectTranslate, isAccountAll } from '@subwallet/extension-base/utils'; @@ -159,7 +159,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); const [autoFormatValue] = useLocalStorage(ADDRESS_INPUT_AUTO_FORMAT_VALUE, false); - const [selectedOption, setSelectedOption] = useState(); + 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')); @@ -203,6 +203,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone const chainStatus = useMemo(() => chainStatusMap[chainValue]?.connectionStatus, [chainValue, chainStatusMap]); const estimatedFee = useMemo((): string => transferInfo?.feeOptions.estimatedFee || '0', [transferInfo]); + console.log('transferInfo', transferInfo); + const [processState, dispatchProcessState] = useReducer(commonProcessReducer, DEFAULT_COMMON_PROCESS); const handleWarning = useCallback((warnings: TransactionWarning[]) => { @@ -462,7 +464,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone value: value, transferAll: options.isTransferAll, transferBounceable: options.isTransferBounceable, - feeOption: selectedOption + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom }); } else { // Make cross chain transfer @@ -475,12 +478,13 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone value, transferAll: options.isTransferAll, transferBounceable: options.isTransferBounceable, - feeOption: selectedOption + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom }); } return sendPromise; - }, [selectedOption]); + }, [selectedTransactionFee]); // todo: must refactor later, temporary solution to support SnowBridge const handleBridgeSpendingApproval = useCallback((values: TransferParams): Promise => { @@ -795,7 +799,8 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone token: assetValue, isXcmTransfer: chainValue !== destChainValue, destChain: destChainValue, - feeOption: selectedOption + feeOption: selectedTransactionFee?.feeOption, + feeCustom: selectedTransactionFee?.feeCustom }, callback) .then((callback)) .catch((e) => { @@ -813,7 +818,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone cancel = true; id && cancelSubscription(id).catch(console.error); }; - }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue, selectedOption]); + }, [assetValue, assetRegistry, chainValue, chainStatus, form, fromValue, destChainValue, selectedTransactionFee]); useEffect(() => { const bnTransferAmount = new BN(transferAmountValue || '0'); @@ -979,7 +984,7 @@ const Component = ({ className = '', isAllAccount, targetAccountProxy }: Compone { diff --git a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx index bb4c4460cf..8dc8ea3704 100644 --- a/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx +++ b/packages/extension-koni-ui/src/components/Field/TransactionFee/FeeEditor/FeeEditorModal.tsx @@ -1,7 +1,7 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { EvmEIP1559FeeOption, FeeDefaultOption, FeeDetail, FeeOption, FeeOptionKey } from '@subwallet/extension-base/types'; +import { EvmEIP1559FeeOption, FeeCustom, FeeDefaultOption, FeeDetail, FeeOptionKey, TransactionFee } from '@subwallet/extension-base/types'; import { BN_ZERO } from '@subwallet/extension-base/utils'; import { BasicInputEvent, RadioGroup } from '@subwallet/extension-koni-ui/components'; import { FeeOptionItem } from '@subwallet/extension-koni-ui/components/Field/TransactionFee/FeeEditor/FeeOptionItem'; @@ -17,7 +17,7 @@ import styled from 'styled-components'; type Props = ThemeProps & { modalId: string; feeOptionsInfo?: FeeDetail; - onSelectOption: (option: FeeOption) => void; + onSelectOption: (option: TransactionFee) => void; symbol: string; decimals: number; tokenSlug: string; @@ -78,20 +78,13 @@ const Component = ({ className, decimals, feeOptionsInfo, modalId, onSelectOptio inactiveModal(modalId); }, [inactiveModal, modalId]); - const _onSelectOption = useCallback((option: FeeOption) => { + const _onSelectOption = useCallback((option: TransactionFee) => {; return () => { onSelectOption(option); inactiveModal(modalId); }; }, [inactiveModal, modalId, onSelectOption]); - const _onSelectCustomOption = useCallback(() => { - return () => { - onSelectOption('custom'); - inactiveModal(modalId); - }; - }, [inactiveModal, modalId, onSelectOption]); - const calculateEstimateFee = useCallback((optionKey: FeeOptionKey) => { const optionValue = feeOptionsInfo?.options?.[optionKey] as EvmEIP1559FeeOption; @@ -109,7 +102,9 @@ const Component = ({ className, decimals, feeOptionsInfo, modalId, onSelectOptio const renderOption = (option: FeeDefaultOption) => { const optionValue = feeOptionsInfo?.options?.[option] as EvmEIP1559FeeOption; const feeValue = calculateEstimateFee(option as FeeOptionKey); - const estimatedWaitTime = (optionValue?.maxWaitTimeEstimate + optionValue?.minWaitTimeEstimate) / 2 || 0; + const estimatedWaitTime = optionValue + ? ((optionValue.maxWaitTimeEstimate || 0) + (optionValue.minWaitTimeEstimate || 0)) / 2 + : 0; return ( ); }; + const _onSelectCustomOption = useCallback(() => { + const customValue = form.getFieldValue('customValue') as FeeCustom; + + return _onSelectOption({ feeCustom: customValue })(); + }, [_onSelectOption, form]); + const customValueValidator = useCallback((rule: Rule, value: string): Promise => { return Promise.resolve(); }, []); @@ -135,11 +136,6 @@ const Component = ({ className, decimals, feeOptionsInfo, modalId, onSelectOptio return BN_ZERO; }, []); - const onSubmitCustomValue: FormCallbacks['onFinish'] = useCallback(({ customValue }: FormProps) => { - inactiveModal(modalId); - onSelectOption('custom'); - }, [inactiveModal, modalId, onSelectOption]); - return (
void; + onSelect?: (option: TransactionFee) => void; isLoading?: boolean; tokenSlug: string; feeOptionsInfo?: FeeDetail; @@ -78,8 +78,8 @@ const Component = ({ className, estimateFee, feeOptionsInfo, isLoading = false, }, 100); }, [activeModal]); - const onSelectOption = useCallback((option: FeeOption) => { - onSelect?.(option); + const onSelectTransactionFee = useCallback((fee: TransactionFee) => { + onSelect?.(fee); }, [onSelect]); const customFieldNode = useMemo(() => { @@ -144,7 +144,7 @@ const Component = ({ className, estimateFee, feeOptionsInfo, isLoading = false, decimals={decimals} feeOptionsInfo={feeOptionsInfo} modalId={modalId} - onSelectOption={onSelectOption} + onSelectOption={onSelectTransactionFee} priceValue={priceValue} symbol={symbol} tokenSlug={tokenSlug}