From 03c0925d82b86254717eecc27331d1925b5fedda Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 10 Jun 2024 11:16:30 +0900 Subject: [PATCH 1/4] Add a chain id validation for `GetChaininfoWithoutEndpointMsg` --- packages/background/src/chains/messages.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/background/src/chains/messages.ts b/packages/background/src/chains/messages.ts index b2242b3a1a..dc3f03172c 100644 --- a/packages/background/src/chains/messages.ts +++ b/packages/background/src/chains/messages.ts @@ -81,7 +81,9 @@ export class GetChainInfoWithoutEndpointsMsg extends Message<{ } validateBasic(): void { - // noop + if (!this.chainId) { + throw new KeplrError("chains", 101, "Chain id not set"); + } } override approveExternal(): boolean { From 7e5637d4cb0e94ad339a1355a9e60eb74639451f Mon Sep 17 00:00:00 2001 From: delivan Date: Mon, 10 Jun 2024 11:24:58 +0900 Subject: [PATCH 2/4] Update ui for evm tx --- .../pages/sign/components/eth-tx/registry.tsx | 2 + .../components/eth-tx/render/send-token.tsx | 138 ++++++++++++++++-- .../sign/components/eth-tx/render/tx-base.tsx | 57 ++++++++ .../src/pages/sign/components/eth-tx/types.ts | 4 + .../src/pages/sign/ethereum/view.tsx | 19 ++- 5 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 apps/extension/src/pages/sign/components/eth-tx/render/tx-base.tsx diff --git a/apps/extension/src/pages/sign/components/eth-tx/registry.tsx b/apps/extension/src/pages/sign/components/eth-tx/registry.tsx index 08d1a4f8dd..d5587f8d12 100644 --- a/apps/extension/src/pages/sign/components/eth-tx/registry.tsx +++ b/apps/extension/src/pages/sign/components/eth-tx/registry.tsx @@ -16,6 +16,8 @@ export class EthTxRenderRegistry implements IEthTxRenderRegistry { chainId: string, unsignedTx: UnsignedTransaction ): { + icon?: React.ReactElement; + title?: string | React.ReactElement; content: string | React.ReactElement; } { try { diff --git a/apps/extension/src/pages/sign/components/eth-tx/render/send-token.tsx b/apps/extension/src/pages/sign/components/eth-tx/render/send-token.tsx index 233a28dc63..6d7657123f 100644 --- a/apps/extension/src/pages/sign/components/eth-tx/render/send-token.tsx +++ b/apps/extension/src/pages/sign/components/eth-tx/render/send-token.tsx @@ -1,11 +1,18 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { FormattedMessage } from "react-intl"; import { useStore } from "../../../../../stores"; import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { erc20ContractInterface } from "@keplr-wallet/stores-eth"; import { BigNumber } from "@ethersproject/bignumber"; import { IEthTxRenderer } from "../types"; +import { ItemLogo } from "../../../../main/token-detail/msg-items/logo"; +import { ColorPalette } from "../../../../../styles"; +import { Box } from "../../../../../components/box"; +import { Body1, Body2, Subtitle4 } from "../../../../../components/typography"; +import { Gutter } from "../../../../../components/gutter"; +import { Columns } from "../../../../../components/column"; +import { Stack } from "../../../../../components/stack"; +import { useTheme } from "styled-components"; export const EthSendTokenTx: IEthTxRenderer = { process(chainId, unsignedTx) { @@ -31,6 +38,29 @@ export const EthSendTokenTx: IEthTxRenderer = { } return { + icon: ( + + + + + } + /> + ), + title: "Send", content: ( + + + + } + /> + ), + title: "Send", content: ( - {chunks}, - }} - /> + + + {amountCoinPretty.trim(true).toString()} + + + + + + + + + + + + + + + From + + {`${sender.slice(0, 10)}...${sender.slice(-8)}`} + + + + To + + {`${recipient.slice(0, 10)}...${recipient.slice(-8)}`} + + + ); }); diff --git a/apps/extension/src/pages/sign/components/eth-tx/render/tx-base.tsx b/apps/extension/src/pages/sign/components/eth-tx/render/tx-base.tsx new file mode 100644 index 0000000000..dcacfa8fa7 --- /dev/null +++ b/apps/extension/src/pages/sign/components/eth-tx/render/tx-base.tsx @@ -0,0 +1,57 @@ +import React, { FunctionComponent } from "react"; +import { useTheme } from "styled-components"; +import { Box } from "../../../../../components/box"; +import { Column, Columns } from "../../../../../components/column"; +import { Gutter } from "../../../../../components/gutter"; +import { Body3, H5 } from "../../../../../components/typography"; +import { ColorPalette } from "../../../../../styles"; + +export const EthTxBase: FunctionComponent<{ + icon: React.ReactElement; + title: string | React.ReactElement; + content: string | React.ReactElement; +}> = ({ icon, title, content }) => { + const theme = useTheme(); + + return ( + + + {icon} + + + + + + +
+ {title} +
+ + + + + {content} + +
+
+
+ ); +}; diff --git a/apps/extension/src/pages/sign/components/eth-tx/types.ts b/apps/extension/src/pages/sign/components/eth-tx/types.ts index a4eb4c7a93..fd2debf282 100644 --- a/apps/extension/src/pages/sign/components/eth-tx/types.ts +++ b/apps/extension/src/pages/sign/components/eth-tx/types.ts @@ -7,6 +7,8 @@ export interface IEthTxRenderer { unsignedTx: UnsignedTransaction ): | { + icon?: React.ReactElement; + title?: string | React.ReactElement; content: string | React.ReactElement; } | undefined; @@ -19,6 +21,8 @@ export interface IEthTxRenderRegistry { chainId: string, unsignedTx: UnsignedTransaction ): { + icon?: React.ReactElement; + title?: string | React.ReactElement; content: string | React.ReactElement; }; } diff --git a/apps/extension/src/pages/sign/ethereum/view.tsx b/apps/extension/src/pages/sign/ethereum/view.tsx index d6c9f0c5b3..c7e18f4a90 100644 --- a/apps/extension/src/pages/sign/ethereum/view.tsx +++ b/apps/extension/src/pages/sign/ethereum/view.tsx @@ -39,6 +39,7 @@ import { useZeroAllowedGasConfig, } from "@keplr-wallet/hooks"; import { CoinPretty, Dec } from "@keplr-wallet/unit"; +import { EthTxBase } from "../components/eth-tx/render/tx-base"; /** * CosmosTxView의 주석을 꼭 참고하셈 @@ -416,14 +417,24 @@ export const EthereumSigningView: FunctionComponent<{ : ColorPalette["gray-100"] } > - { - defaultRegistry.render( + {(() => { + const { icon, title, content } = defaultRegistry.render( interactionData.data.chainId, JSON.parse( Buffer.from(interactionData.data.message).toString() ) as UnsignedTransaction - ).content - } + ); + + if (icon !== undefined && title !== undefined) { + return ( + + ); + } + })()} )} From a2e9f1d3a4c4a5329c08c3f05e6f32dbc2ebef31 Mon Sep 17 00:00:00 2001 From: delivan Date: Wed, 19 Jun 2024 15:55:52 +0900 Subject: [PATCH 3/4] Make fee estimated if no fee info in evm tx --- .../src/pages/sign/ethereum/view.tsx | 204 ++++++++++++------ 1 file changed, 144 insertions(+), 60 deletions(-) diff --git a/apps/extension/src/pages/sign/ethereum/view.tsx b/apps/extension/src/pages/sign/ethereum/view.tsx index c7e18f4a90..cb6e4f289b 100644 --- a/apps/extension/src/pages/sign/ethereum/view.tsx +++ b/apps/extension/src/pages/sign/ethereum/view.tsx @@ -1,4 +1,10 @@ -import React, { FunctionComponent, useMemo, useRef, useState } from "react"; +import React, { + FunctionComponent, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { SignEthereumInteractionStore } from "@keplr-wallet/stores-core"; import { Box } from "../../../components/box"; import { XAxis } from "../../../components/axis"; @@ -35,11 +41,14 @@ import { FeeControl } from "../../../components/input/fee-control"; import { useAmountConfig, useFeeConfig, + useGasSimulator, useSenderConfig, useZeroAllowedGasConfig, } from "@keplr-wallet/hooks"; -import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { EthTxBase } from "../components/eth-tx/render/tx-base"; +import { erc20ContractInterface } from "@keplr-wallet/stores-eth"; +import { ExtensionKVStore } from "@keplr-wallet/common"; +import { CoinPretty, Dec, Int, IntPretty } from "@keplr-wallet/unit"; /** * CosmosTxView의 주석을 꼭 참고하셈 @@ -54,6 +63,8 @@ export const EthereumSigningView: FunctionComponent<{ chainStore, uiConfigStore, signEthereumInteractionStore, + accountStore, + ethereumAccountStore, queriesStore, } = useStore(); const intl = useIntl(); @@ -63,59 +74,157 @@ export const EthereumSigningView: FunctionComponent<{ signEthereumInteractionStore.rejectAll(); }); - const chainInfo = chainStore.getChain(interactionData.data.chainId); + const { message, signType, signer, chainId } = interactionData.data; - const senderConfig = useSenderConfig( - chainStore, - interactionData.data.chainId, - interactionData.data.signer - ); - const gasConfig = useZeroAllowedGasConfig(chainStore, chainInfo.chainId, 0); + const account = accountStore.getAccount(chainId); + const ethereumAccount = ethereumAccountStore.getAccount(chainId); + const chainInfo = chainStore.getChain(chainId); + + const senderConfig = useSenderConfig(chainStore, chainId, signer); + const gasConfig = useZeroAllowedGasConfig(chainStore, chainId, 0); const amountConfig = useAmountConfig( chainStore, queriesStore, - chainInfo.chainId, + chainId, senderConfig ); const feeConfig = useFeeConfig( chainStore, queriesStore, - chainInfo.chainId, + chainId, senderConfig, amountConfig, gasConfig ); - const isTxSigning = interactionData.data.signType === EthSignType.TRANSACTION; + const [signingDataBuff, setSigningDataBuff] = useState(Buffer.from(message)); + const isTxSigning = signType === EthSignType.TRANSACTION; + + const gasSimulator = useGasSimulator( + new ExtensionKVStore("gas-simulator.ethereum.sign"), + chainStore, + chainInfo.chainId, + gasConfig, + feeConfig, + "evm/native", + () => { + const unsignedTx = JSON.parse(Buffer.from(message).toString("utf8")); + + const { currency, recipient, amount } = (() => { + if (unsignedTx.data) { + const dataDecodedValues = erc20ContractInterface.decodeFunctionData( + "transfer", + unsignedTx.data + ); + const erc20ContractAddress = unsignedTx.to; + const currency = chainInfo.forceFindCurrency( + `erc20:${erc20ContractAddress}` + ); + const recipient = dataDecodedValues[0]; + const amount = new IntPretty(new Dec(parseInt(dataDecodedValues[1]))) + .moveDecimalPointLeft(currency.coinDecimals) + .toString(); - const eip1559TxFee = (() => { + return { currency, recipient, amount }; + } else { + const currency = chainInfo.currencies[0]; + const recipient = unsignedTx.to; + const amount = new IntPretty(new Dec(parseInt(unsignedTx.value))) + .moveDecimalPointLeft(currency.coinDecimals) + .toString(); + + return { + currency, + recipient, + amount, + }; + } + })(); + + return { + simulate: () => + ethereumAccount.simulateGas({ + sender: account.ethereumHexAddress, + currency, + amount, + recipient, + }), + }; + } + ); + + const maxPriorityFeePerGas = (() => { if (isTxSigning) { - const { maxFeePerGas, maxPriorityFeePerGas } = feeConfig.getEIP1559TxFees( + const { maxPriorityFeePerGas } = feeConfig.getEIP1559TxFees( feeConfig.type === "manual" ? uiConfigStore.lastFeeOption || "average" : feeConfig.type ); - return { - maxFeePerGas: `0x${parseInt(maxFeePerGas.toString()).toString(16)}`, - maxPriorityFeePerGas: `0x${parseInt( - maxPriorityFeePerGas.toString() - ).toString(16)}`, - }; + return `0x${parseInt(maxPriorityFeePerGas.toString()).toString(16)}`; } })(); - const signingDataText = useMemo(() => { - const messageBuff = Buffer.from(interactionData.data.message); + const [isFeeSelectedOnSelector, setIsFeeSelectedOnSelector] = useState(false); + + useEffect(() => { + if (isTxSigning) { + const unsignedTx = JSON.parse(Buffer.from(message).toString("utf8")); + + const gasLimitFromTx = unsignedTx.gasLimit ?? unsignedTx.gas; + if (gasLimitFromTx) { + gasSimulator.setEnabled(false); + gasConfig.setValue(parseInt(gasLimitFromTx)); + } + + if ( + gasConfig.gas > 0 && + unsignedTx.maxFeePerGas && + isFeeSelectedOnSelector === false + ) { + feeConfig.setFee( + new CoinPretty( + chainInfo.currencies[0], + new Dec(gasConfig.gas).mul( + new Dec(parseInt(unsignedTx.maxFeePerGas)) + ) + ) + ); + setIsFeeSelectedOnSelector(true); + } - switch (interactionData.data.signType) { + unsignedTx.gasLimit = `0x${gasConfig.gas.toString(16)}`; + unsignedTx.maxFeePerGas = `0x${new Int( + feeConfig.getFeePrimitive()[0].amount + ) + .div(new Int(gasConfig.gas)) + .toBigNumber() + .toString(16)}`; + unsignedTx.maxPriorityFeePerGas = + unsignedTx.maxPriorityFeePerGas ?? maxPriorityFeePerGas; + setSigningDataBuff(Buffer.from(JSON.stringify(unsignedTx), "utf8")); + } + }, [ + gasConfig.gas, + isTxSigning, + message, + maxPriorityFeePerGas, + gasSimulator, + gasConfig, + feeConfig, + chainInfo.currencies, + isFeeSelectedOnSelector, + ]); + + const signingDataText = useMemo(() => { + switch (signType) { case EthSignType.MESSAGE: // If the message is 32 bytes, it's probably a hash. - if (messageBuff.length === 32) { - return messageBuff.toString("hex"); + if (signingDataBuff.length === 32) { + return signingDataBuff.toString("hex"); } else { const text = (() => { - const string = messageBuff.toString("utf8"); + const string = signingDataBuff.toString("utf8"); if (string.startsWith("0x")) { return Buffer.from(string.slice(2), "hex").toString("utf8"); } @@ -127,43 +236,17 @@ export const EthereumSigningView: FunctionComponent<{ return text.replace(/\u202E/giu, "\\u202E"); } case EthSignType.TRANSACTION: - const unsignedTx = JSON.parse( - messageBuff.toString() - ) as UnsignedTransaction; - - const gasLimit = parseInt(String(unsignedTx.gasLimit) ?? "0"); - gasConfig.setValue(gasLimit); - if (unsignedTx.maxFeePerGas && feeConfig.type === "manual") { - feeConfig.setFee( - new CoinPretty( - chainInfo.feeCurrencies[0], - new Dec(parseInt(String(unsignedTx.maxFeePerGas ?? "0"))).mul( - new Dec(gasLimit) - ) - ) - ); - } else { - if (eip1559TxFee) { - unsignedTx.maxFeePerGas = eip1559TxFee.maxFeePerGas; - unsignedTx.maxPriorityFeePerGas = eip1559TxFee.maxPriorityFeePerGas; - } - } - - return JSON.stringify(unsignedTx, null, 2); + return JSON.stringify( + JSON.parse(signingDataBuff.toString("utf8")), + null, + 2 + ); case EthSignType.EIP712: - return JSON.stringify(JSON.parse(messageBuff.toString()), null, 2); + return JSON.stringify(JSON.parse(signingDataBuff.toString()), null, 2); default: - return messageBuff.toString("hex"); + return signingDataBuff.toString("hex"); } - }, [ - chainInfo.feeCurrencies, - eip1559TxFee?.maxFeePerGas, - eip1559TxFee?.maxPriorityFeePerGas, - feeConfig.type, - gasConfig, - interactionData.data.message, - interactionData.data.signType, - ]); + }, [signingDataBuff, signType]); const [isViewData, setIsViewData] = useState(false); @@ -471,6 +554,7 @@ export const EthereumSigningView: FunctionComponent<{ feeConfig={feeConfig} senderConfig={senderConfig} gasConfig={gasConfig} + gasSimulator={gasSimulator} /> ); })()} From 29e7fdc6351bfeff790d8b8df6a41b50aa0642d5 Mon Sep 17 00:00:00 2001 From: delivan Date: Fri, 21 Jun 2024 14:17:41 +0900 Subject: [PATCH 4/4] Modify to not save gas estimates --- apps/extension/src/pages/sign/ethereum/view.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/pages/sign/ethereum/view.tsx b/apps/extension/src/pages/sign/ethereum/view.tsx index cb6e4f289b..3db1023fc4 100644 --- a/apps/extension/src/pages/sign/ethereum/view.tsx +++ b/apps/extension/src/pages/sign/ethereum/view.tsx @@ -47,7 +47,7 @@ import { } from "@keplr-wallet/hooks"; import { EthTxBase } from "../components/eth-tx/render/tx-base"; import { erc20ContractInterface } from "@keplr-wallet/stores-eth"; -import { ExtensionKVStore } from "@keplr-wallet/common"; +import { MemoryKVStore } from "@keplr-wallet/common"; import { CoinPretty, Dec, Int, IntPretty } from "@keplr-wallet/unit"; /** @@ -101,7 +101,7 @@ export const EthereumSigningView: FunctionComponent<{ const isTxSigning = signType === EthSignType.TRANSACTION; const gasSimulator = useGasSimulator( - new ExtensionKVStore("gas-simulator.ethereum.sign"), + new MemoryKVStore("gas-simulator.ethereum.sign"), chainStore, chainInfo.chainId, gasConfig,