diff --git a/apps/extension/src/components/input/fee-control/index.tsx b/apps/extension/src/components/input/fee-control/index.tsx index 931454963f..2e06375740 100644 --- a/apps/extension/src/components/input/fee-control/index.tsx +++ b/apps/extension/src/components/input/fee-control/index.tsx @@ -215,7 +215,12 @@ export const FeeControl: FunctionComponent<{ const [isModalOpen, setIsModalOpen] = useState(false); - const isShowingEstimatedFee = isForEVMTx && !!gasSimulator?.gasEstimated; + // EVM 트랜잭션의 경우, 외부에서 fee를 설정한 경우를 구분하기 위해서 사용 + const isFeeSetByUser = isForEVMTx && feeConfig.type !== "manual"; + + // gasAdjustment와 gasEstimated를 사용해 계산된 값을 보여주는 경우 + const isShowingFeeWithGasEstimated = + !!gasSimulator?.enabled && !!gasSimulator?.gasEstimated && isFeeSetByUser; return ( @@ -274,18 +279,40 @@ export const FeeControl: FunctionComponent<{ })() .map((fee) => fee + .sub( + new Dec(feeConfig.l1DataFee?.toString() || "0") + ) .quo( new Dec( - isShowingEstimatedFee ? gasConfig?.gas || 1 : 1 + isShowingFeeWithGasEstimated + ? gasConfig?.gas || 1 + : 1 ) ) .mul( new Dec( - isShowingEstimatedFee + isShowingFeeWithGasEstimated ? gasSimulator?.gasEstimated || 1 : 1 ) ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) + ) + .add( + new Dec(feeConfig.l1DataFee?.toString() || "0") + ) + .add( + isFeeSetByUser + ? new Dec(0) + : new Dec( + feeConfig.l1DataFee?.toString() || "0" + ) // evm fee가 외부에서 설정된 경우, fee = gasLimit * gasPrice이므로 l1DataFee를 더해줘야 함 + ) .maxDecimals(6) .inequalitySymbol(true) .trim(true) @@ -320,18 +347,34 @@ export const FeeControl: FunctionComponent<{ } else { const price = priceStore.calculatePrice( fee + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) .quo( new Dec( - isShowingEstimatedFee ? gasConfig?.gas || 1 : 1 + isShowingFeeWithGasEstimated + ? gasConfig?.gas || 1 + : 1 ) ) .mul( new Dec( - isShowingEstimatedFee + isShowingFeeWithGasEstimated ? gasSimulator?.gasEstimated || 1 : 1 ) ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) + ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .add( + isFeeSetByUser + ? new Dec(0) + : new Dec(feeConfig.l1DataFee?.toString() || "0") + ) ); if (price) { if (!total) { diff --git a/apps/extension/src/components/input/fee-control/modal.tsx b/apps/extension/src/components/input/fee-control/modal.tsx index 9a91a85a79..55af844cf3 100644 --- a/apps/extension/src/components/input/fee-control/modal.tsx +++ b/apps/extension/src/components/input/fee-control/modal.tsx @@ -143,7 +143,10 @@ export const TransactionFeeModal: FunctionComponent<{ isGasSimulatorEnabled, ]); - const isShowingMaxFee = isForEVMTx && !!gasSimulator?.gasEstimated; + const isShowingMaxFee = isForEVMTx; + const isFeeSetByUser = isForEVMTx && feeConfig.type !== "manual"; + const isShowingFeeWithGasEstimated = + !!isGasSimulatorEnabled && !!gasSimulator?.gasEstimated && isFeeSetByUser; return ( @@ -207,7 +210,7 @@ export const TransactionFeeModal: FunctionComponent<{ feeConfig={feeConfig} gasConfig={gasConfig} gasSimulator={gasSimulator} - isForEVMTx={isForEVMTx} + isShowingFeeWithGasEstimated={isShowingFeeWithGasEstimated} /> @@ -226,6 +229,11 @@ export const TransactionFeeModal: FunctionComponent<{ {`: ${feeConfig.fees[0] + .sub( + isFeeSetByUser + ? new Dec(feeConfig.l1DataFee?.toString() || "0") + : new Dec(0) + ) .maxDecimals(6) .inequalitySymbol(true) .trim(true) @@ -246,7 +254,11 @@ export const TransactionFeeModal: FunctionComponent<{ {` ${(() => { let total: PricePretty | undefined; let hasUnknown = false; - const maxFee = feeConfig.fees[0]; + const maxFee = feeConfig.fees[0].sub( + isFeeSetByUser + ? new Dec(feeConfig.l1DataFee?.toString() || "0") + : new Dec(0) + ); if (!maxFee.currency.coinGeckoId) { hasUnknown = true; } else { @@ -500,222 +512,288 @@ const FeeSelector: FunctionComponent<{ feeConfig: IFeeConfig; gasConfig?: IGasConfig; gasSimulator?: IGasSimulator; - isForEVMTx?: boolean; -}> = observer(({ feeConfig, gasConfig, gasSimulator, isForEVMTx }) => { - const { priceStore } = useStore(); - const theme = useTheme(); + isShowingFeeWithGasEstimated?: boolean; +}> = observer( + ({ feeConfig, gasConfig, gasSimulator, isShowingFeeWithGasEstimated }) => { + const { priceStore } = useStore(); + const theme = useTheme(); - const feeCurrency = - feeConfig.fees.length > 0 - ? feeConfig.fees[0].currency - : feeConfig.selectableFeeCurrencies[0]; + const feeCurrency = + feeConfig.fees.length > 0 + ? feeConfig.fees[0].currency + : feeConfig.selectableFeeCurrencies[0]; - if (!feeCurrency) { - return null; - } + if (!feeCurrency) { + return null; + } - const isShowingGasEstimatedOnly = isForEVMTx && !!gasSimulator?.gasEstimated; - - return ( - - - { - feeConfig.setFee({ - type: "low", - currency: feeCurrency, - }); - }} - selected={feeConfig.type === "low"} - > - {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} - - - - - {feeCurrency.coinGeckoId ? ( - - {priceStore - .calculatePrice( - feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "low") - .quo( - new Dec( - isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1 + return ( + + + { + feeConfig.setFee({ + type: "low", + currency: feeCurrency, + }); + }} + selected={feeConfig.type === "low"} + > + {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} + + + + + {feeCurrency.coinGeckoId ? ( + + {priceStore + .calculatePrice( + feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "low") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated + ? gasConfig?.gas || 1 + : 1 + ) + ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) ) - ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + ) + ?.toString() || "-"} + + ) : null} + + {feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "low") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated ? gasConfig?.gas || 1 : 1 + ) + ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ?.toString() || "-"} - - ) : null} - - {feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "low") - .quo( - new Dec(isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) ) - ) - .maxDecimals(6) - .inequalitySymbol(true) - .trim(true) - .shrink(true) - .hideIBCMetadata(true) - .toString()} - - - - - - - { - feeConfig.setFee({ - type: "average", - currency: feeCurrency, - }); - }} - selected={feeConfig.type === "average"} - > - {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} - - - - - {feeCurrency.coinGeckoId ? ( - - {priceStore - .calculatePrice( - feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "average") - .quo( - new Dec( - isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1 + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .shrink(true) + .hideIBCMetadata(true) + .toString()} + + + + + + + { + feeConfig.setFee({ + type: "average", + currency: feeCurrency, + }); + }} + selected={feeConfig.type === "average"} + > + {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} + + + + + {feeCurrency.coinGeckoId ? ( + + {priceStore + .calculatePrice( + feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "average") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated + ? gasConfig?.gas || 1 + : 1 + ) + ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) ) - ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + ) + ?.toString() || "-"} + + ) : null} + + {feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "average") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated ? gasConfig?.gas || 1 : 1 + ) ) - ?.toString() || "-"} - - ) : null} - - {feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "average") - .quo( - new Dec(isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ) - .maxDecimals(6) - .inequalitySymbol(true) - .trim(true) - .shrink(true) - .hideIBCMetadata(true) - .toString()} - - - - - - - { - feeConfig.setFee({ - type: "high", - currency: feeCurrency, - }); - }} - selected={feeConfig.type === "high"} - > - {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} - - - - - {feeCurrency.coinGeckoId ? ( - - {priceStore - .calculatePrice( - feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "high") - .quo( - new Dec( - isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) + ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .shrink(true) + .hideIBCMetadata(true) + .toString()} + + + + + + + { + feeConfig.setFee({ + type: "high", + currency: feeCurrency, + }); + }} + selected={feeConfig.type === "high"} + > + {/* 텍스트의 길이 등에 의해서 레이아웃이 변하는걸 막기 위해서 가라로 1px의 너비르 가지는 Box로 감싸준다. */} + + + + + {feeCurrency.coinGeckoId ? ( + + {priceStore + .calculatePrice( + feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "high") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated + ? gasConfig?.gas || 1 + : 1 + ) ) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) + ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + ) + ?.toString() || "-"} + + ) : null} + + {feeConfig + .getFeeTypePrettyForFeeCurrency(feeCurrency, "high") + .sub(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .quo( + new Dec( + isShowingFeeWithGasEstimated ? gasConfig?.gas || 1 : 1 + ) + ) + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasAdjustment || 1 + : 1 + ) ) - ?.toString() || "-"} - - ) : null} - - {feeConfig - .getFeeTypePrettyForFeeCurrency(feeCurrency, "high") - .quo( - new Dec(isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 + .mul( + new Dec( + isShowingFeeWithGasEstimated + ? gasSimulator?.gasEstimated || 1 + : 1 + ) ) - ) - .maxDecimals(6) - .inequalitySymbol(true) - .trim(true) - .shrink(true) - .hideIBCMetadata(true) - .toString()} - - - - - - ); -}); + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) + .maxDecimals(6) + .inequalitySymbol(true) + .trim(true) + .shrink(true) + .hideIBCMetadata(true) + .toString()} + + + + + + ); + } +); diff --git a/apps/extension/src/pages/sign/components/fee-summary/index.tsx b/apps/extension/src/pages/sign/components/fee-summary/index.tsx index c149cb76fa..f8d48f0574 100644 --- a/apps/extension/src/pages/sign/components/fee-summary/index.tsx +++ b/apps/extension/src/pages/sign/components/fee-summary/index.tsx @@ -29,8 +29,6 @@ export const FeeSummary: FunctionComponent<{ const theme = useTheme(); - const isShowingGasEstimatedOnly = isForEVMTx && !!gasSimulator?.gasEstimated; - return ( fee - .quo( - new Dec(isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 - ) - ) + .add(new Dec(feeConfig.l1DataFee?.toString() || "0")) .maxDecimals(6) .inequalitySymbol(true) .trim(true) @@ -134,19 +123,7 @@ export const FeeSummary: FunctionComponent<{ break; } else { const price = priceStore.calculatePrice( - fee - .quo( - new Dec( - isShowingGasEstimatedOnly ? gasConfig?.gas || 1 : 1 - ) - ) - .mul( - new Dec( - isShowingGasEstimatedOnly - ? gasSimulator?.gasEstimated || 1 - : 1 - ) - ) + fee.add(new Dec(feeConfig.l1DataFee?.toString() || "0")) ); if (price) { if (!total) { diff --git a/apps/extension/src/pages/sign/ethereum/view.tsx b/apps/extension/src/pages/sign/ethereum/view.tsx index 067a287013..39828e9813 100644 --- a/apps/extension/src/pages/sign/ethereum/view.tsx +++ b/apps/extension/src/pages/sign/ethereum/view.tsx @@ -43,12 +43,13 @@ import { useFeeConfig, useGasSimulator, useSenderConfig, + useTxConfigsValidate, useZeroAllowedGasConfig, } from "@keplr-wallet/hooks"; import { handleExternalInteractionWithNoProceedNext } from "../../../utils"; import { EthTxBase } from "../components/eth-tx/render/tx-base"; import { MemoryKVStore } from "@keplr-wallet/common"; -import { CoinPretty, Dec, Int } from "@keplr-wallet/unit"; +import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { Image } from "../../../components/image"; import { Column, Columns } from "../../../components/column"; import { useNavigate } from "react-router"; @@ -108,6 +109,7 @@ export const EthereumSigningView: FunctionComponent<{ ); const [signingDataBuff, setSigningDataBuff] = useState(Buffer.from(message)); + const [preferNoSetFee, setPreferNoSetFee] = useState(false); const isTxSigning = signType === EthSignType.TRANSACTION; const gasSimulator = useGasSimulator( @@ -188,12 +190,16 @@ export const EthereumSigningView: FunctionComponent<{ unsignedTx.maxFeePerGas ?? unsignedTx.gasPrice ?? 0 ); if (gasPriceFromTx > 0) { + // 사이트에서 제공된 수수료를 사용하는 경우, fee type이 manual로 설정되며, + // 사용자가 수동으로 설정하는 것을 지양하기 위해 preferNoSetFee를 true로 설정 feeConfig.setFee( new CoinPretty( chainInfo.currencies[0], new Dec(gasConfig.gas).mul(new Dec(gasPriceFromTx)) ) ); + + setPreferNoSetFee(!interactionData.isInternal); } } } @@ -204,34 +210,26 @@ export const EthereumSigningView: FunctionComponent<{ if (isTxSigning && !interactionData.isInternal) { const unsignedTx = JSON.parse(Buffer.from(message).toString("utf8")); + // 수수료 옵션을 사이트에서 제공하는 경우, 수수료 옵션을 사용하지 않음 + if (feeConfig.type === "manual") { + return; + } + if (gasConfig.gas > 0) { unsignedTx.gasLimit = `0x${gasConfig.gas.toString(16)}`; + } - if (!unsignedTx.maxFeePerGas && !unsignedTx.gasPrice) { - unsignedTx.maxFeePerGas = `0x${new Int( - feeConfig.getFeePrimitive()[0].amount - ) - .div(new Int(gasConfig.gas)) - .toBigNumber() - .toString(16)}`; - } + // EIP-1559 우선 적용 + if (maxFeePerGas) { + unsignedTx.gasPrice = undefined; + unsignedTx.maxFeePerGas = maxFeePerGas; } - if ( - !unsignedTx.maxPriorityFeePerGas && - !unsignedTx.gasPrice && - maxPriorityFeePerGas - ) { - unsignedTx.maxPriorityFeePerGas = - unsignedTx.maxPriorityFeePerGas ?? maxPriorityFeePerGas; + if (unsignedTx.maxFeePerGas && maxPriorityFeePerGas) { + unsignedTx.maxPriorityFeePerGas = maxPriorityFeePerGas; } - if ( - !unsignedTx.gasPrice && - !unsignedTx.maxFeePerGas && - !unsignedTx.maxPriorityFeePerGas && - gasPrice - ) { + if (!maxFeePerGas && !maxPriorityFeePerGas && gasPrice) { unsignedTx.gasPrice = gasPrice; } @@ -247,6 +245,7 @@ export const EthereumSigningView: FunctionComponent<{ gasSimulator, gasConfig, feeConfig, + feeConfig.type, interactionData.isInternal, ]); @@ -357,6 +356,12 @@ export const EthereumSigningView: FunctionComponent<{ const [isUnknownContractExecution, setIsUnknownContractExecution] = useState(false); + const txConfigsValidate = useTxConfigsValidate({ + senderConfig, + gasConfig, + feeConfig, + }); + const isLoading = signEthereumInteractionStore.isObsoleteInteractionApproved( interactionData.id @@ -364,6 +369,8 @@ export const EthereumSigningView: FunctionComponent<{ isLedgerInteracting || isKeystoneInteracting; + const buttonDisabled = txConfigsValidate.interactionBlocked; + return ( , + disabled: buttonDisabled, isLoading, onClick: async () => { try { @@ -742,6 +750,7 @@ export const EthereumSigningView: FunctionComponent<{ senderConfig={senderConfig} gasConfig={gasConfig} gasSimulator={gasSimulator} + disableAutomaticFeeSet={preferNoSetFee} isForEVMTx /> ); diff --git a/packages/stores-eth/src/account/base.ts b/packages/stores-eth/src/account/base.ts index 8c608a25e6..686d72beb8 100644 --- a/packages/stores-eth/src/account/base.ts +++ b/packages/stores-eth/src/account/base.ts @@ -21,22 +21,7 @@ import { Interface } from "@ethersproject/abi"; const opStackGasPriceOracleProxyAddress = "0x420000000000000000000000000000000000000F"; -const opStackGasPriceOracleProxyABI = new Interface([ - { - constant: true, - inputs: [], - name: "implementation", - outputs: [ - { - name: "", - type: "address", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, -]); + const opStackGasPriceOracleABI = new Interface([ { inputs: [{ internalType: "bytes", name: "_data", type: "bytes" }], @@ -153,30 +138,17 @@ export class EthereumAccountBase { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const keplr = (await this.getKeplr())!; - const implementationAddress = await keplr.ethereum.request({ - method: "eth_call", - params: [ - { - to: opStackGasPriceOracleProxyAddress, - data: opStackGasPriceOracleProxyABI.encodeFunctionData( - "implementation" - ), - }, - ], - chainId: this.chainId, - }); - const gasPriceOracleContractAddress = - "0x" + implementationAddress.slice(26); const l1Fee = await keplr.ethereum.request({ method: "eth_call", params: [ { - to: gasPriceOracleContractAddress, + to: opStackGasPriceOracleProxyAddress, data: opStackGasPriceOracleABI.encodeFunctionData("getL1Fee", [ serialize(unsignedTx), ]), }, + "latest", ], chainId: this.chainId, });