diff --git a/apps/extension/src/pages/main/token-detail/modal.tsx b/apps/extension/src/pages/main/token-detail/modal.tsx index d92859d0df..db4b738505 100644 --- a/apps/extension/src/pages/main/token-detail/modal.tsx +++ b/apps/extension/src/pages/main/token-detail/modal.tsx @@ -673,30 +673,32 @@ export const TokenDetailModal: FunctionComponent<{ if (msgHistory.pages[0].response?.isUnsupported || !isSupported) { // TODO: 아직 cosmos 체인이 아니면 embedded인지 아닌지 구분할 수 없다. - if ("cosmos" in modularChainInfo) { - const chainInfo = chainStore.getChain(chainId); - if (chainInfo.embedded.embedded) { - return ( - - - - - Unsupported Chain - - - {`We're actively working on expanding our support for + if ( + ("cosmos" in modularChainInfo && + chainStore.getChain(chainId).embedded.embedded) || + "starknet" in modularChainInfo + ) { + return ( + + + + + Unsupported Chain + + + {`We're actively working on expanding our support for native chains.`} - - - - - ); - } + + + + + ); } + return ( diff --git a/apps/extension/src/pages/starknet/components/account-activation-modal/index.tsx b/apps/extension/src/pages/starknet/components/account-activation-modal/index.tsx index c9cd4efe54..5e8996b673 100644 --- a/apps/extension/src/pages/starknet/components/account-activation-modal/index.tsx +++ b/apps/extension/src/pages/starknet/components/account-activation-modal/index.tsx @@ -61,6 +61,7 @@ export const AccountActivationModal: FunctionComponent<{ const intl = useIntl(); const account = accountStore.getAccount(chainId); + const starknetQueries = starknetQueriesStore.get(chainId); const sender = account.starknetHexAddress; const senderConfig = useSenderConfig( @@ -102,6 +103,22 @@ export const AccountActivationModal: FunctionComponent<{ return () => clearInterval(interval); }, [gasSimulationRefresher]); + useEffect(() => { + starknetQueries.queryAccountNonce + .getNonce(account.starknetHexAddress) + .fetch(); + if (feeConfig.fee != null) { + starknetQueries.queryStarknetERC20Balance + .getBalance( + chainId, + chainStore, + account.starknetHexAddress, + feeConfig.fee.currency.coinMinimalDenom + ) + ?.fetch(); + } + }, []); + const gasSimulatorKey = feeConfig.type; const gasSimulator = useGasSimulator( new ExtensionKVStore("gas-simulator.starknet.account-activation"), @@ -283,39 +300,38 @@ export const AccountActivationModal: FunctionComponent<{ throw new Error("Can't find fee currency"); } + starknetAccount.setIsDeployingAccount(true); const { transaction_hash: txHash } = - await starknetAccountStore - .getAccount(senderConfig.chainId) - .deployAccountWithFee( - accountStore.getAccount(senderConfig.chainId) - .starknetHexAddress, - "0x" + Buffer.from(params.classHash).toString("hex"), - [ - "0x" + Buffer.from(params.xLow).toString("hex"), - "0x" + Buffer.from(params.xHigh).toString("hex"), - "0x" + Buffer.from(params.yLow).toString("hex"), - "0x" + Buffer.from(params.yHigh).toString("hex"), - ], - "0x" + Buffer.from(params.salt).toString("hex"), - (() => { - if (type === "ETH") { - return { - type: "ETH", - maxFee: feeConfig.maxFee.toCoin().amount, - }; - } else if (type === "STRK") { - return { - type: "STRK", - gas: gasConfig.gas.toString(), - maxGasPrice: num.toHex( - feeConfig.maxGasPrice.toCoin().amount - ), - }; - } else { - throw new Error("Invalid fee type"); - } - })() - ); + await starknetAccount.deployAccountWithFee( + accountStore.getAccount(senderConfig.chainId) + .starknetHexAddress, + "0x" + Buffer.from(params.classHash).toString("hex"), + [ + "0x" + Buffer.from(params.xLow).toString("hex"), + "0x" + Buffer.from(params.xHigh).toString("hex"), + "0x" + Buffer.from(params.yLow).toString("hex"), + "0x" + Buffer.from(params.yHigh).toString("hex"), + ], + "0x" + Buffer.from(params.salt).toString("hex"), + (() => { + if (type === "ETH") { + return { + type: "ETH", + maxFee: feeConfig.maxFee.toCoin().amount, + }; + } else if (type === "STRK") { + return { + type: "STRK", + gas: gasConfig.gas.toString(), + maxGasPrice: num.toHex( + feeConfig.maxGasPrice.toCoin().amount + ), + }; + } else { + throw new Error("Invalid fee type"); + } + })() + ); new InExtensionMessageRequester() .sendMessage( @@ -330,7 +346,6 @@ export const AccountActivationModal: FunctionComponent<{ }), "" ); - const starknetQueries = starknetQueriesStore.get(chainId); @@ -343,6 +358,19 @@ export const AccountActivationModal: FunctionComponent<{ .getNonce(account.starknetHexAddress) .waitFreshResponse(); if (res?.data) { + starknetAccount.setIsDeployingAccount(false); + + if (feeConfig.fee != null) { + starknetQueries.queryStarknetERC20Balance + .getBalance( + chainId, + chainStore, + account.starknetHexAddress, + feeConfig.fee.currency.coinMinimalDenom + ) + ?.fetch(); + } + close(); break; } @@ -351,26 +379,15 @@ export const AccountActivationModal: FunctionComponent<{ await sleep(2000); } })(); - - if (feeConfig.fee != null) { - starknetQueries.queryStarknetERC20Balance - .getBalance( - chainId, - chainStore, - account.starknetHexAddress, - feeConfig.fee.currency.coinMinimalDenom - ) - ?.fetch(); - } - - close(); }) .catch((e) => { - // 이 경우에는 tx가 커밋된 이후의 오류이기 때문에 이미 페이지는 sign 페이지에서부터 전환된 상태다. - // 따로 멀 처리해줄 필요가 없다 + starknetAccount.setIsDeployingAccount(false); + close(); console.log(e); }); } catch (e) { + starknetAccount.setIsDeployingAccount(false); + goBack(); console.log(e); } } diff --git a/apps/extension/src/pages/starknet/components/input/fee-control/index.tsx b/apps/extension/src/pages/starknet/components/input/fee-control/index.tsx index 6cc28c8350..eb384469de 100644 --- a/apps/extension/src/pages/starknet/components/input/fee-control/index.tsx +++ b/apps/extension/src/pages/starknet/components/input/fee-control/index.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useState } from "react"; +import React, { FunctionComponent, useLayoutEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { IFeeConfig, @@ -23,8 +23,11 @@ import { VerticalResizeTransition } from "../../../../../components/transition"; import { FormattedMessage, useIntl } from "react-intl"; import { XAxis, YAxis } from "../../../../../components/axis"; import { UIConfigStore } from "../../../../../stores/ui-config"; -import { IChainStore, IQueriesStore } from "@keplr-wallet/stores"; +import { IChainStore } from "@keplr-wallet/stores"; import { Tooltip } from "../../../../../components/tooltip"; +import { autorun } from "mobx"; +import { StarknetQueriesStore } from "@keplr-wallet/stores-starknet"; +import { Dec } from "@keplr-wallet/unit"; // 기본적으로 `FeeControl` 안에 있는 로직이였지만 `FeeControl` 말고도 다른 UI를 가진 똑같은 기능의 component가 // 여러개 생기게 되면서 공통적으로 사용하기 위해서 custom hook으로 분리함 @@ -66,112 +69,84 @@ export const useFeeOptionSelectionOnInit = ( }; export const useAutoFeeCurrencySelectionOnInit = ( - _chainStore: IChainStore, - _queriesStore: IQueriesStore, - _senderConfig: ISenderConfig, - _feeConfig: IFeeConfig, - _disableAutomaticFeeSet: boolean | undefined + chainStore: IChainStore, + starknetQueriesStore: StarknetQueriesStore, + senderConfig: ISenderConfig, + feeConfig: IFeeConfig, + disableAutomaticFeeSet: boolean | undefined ) => { - // TODO - // useLayoutEffect(() => { - // if (disableAutomaticFeeSet) { - // return; - // } - // - // // Require to invoke effect whenever chain is changed, - // // even though it is not used in logic. - // noop(feeConfig.chainId); - // - // // Try to find other fee currency if the account doesn't have enough fee to pay. - // // This logic can be slightly complex, so use mobx's `autorun`. - // // This part fairly different with the approach of react's hook. - // let skip = false; - // // Try until 500ms to avoid the confusion to user. - // const timeoutId = setTimeout(() => { - // skip = true; - // }, 2000); - // - // const disposer = autorun(() => { - // if ( - // !skip && - // feeConfig.type !== "manual" && - // feeConfig.selectableFeeCurrencies.length > 1 && - // feeConfig.fees.length > 0 - // ) { - // const queryBalances = - // chainStore.getChain(feeConfig.chainId).evm != null && - // EthereumAccountBase.isEthereumHexAddressWithChecksum( - // senderConfig.sender - // ) - // ? queriesStore - // .get(feeConfig.chainId) - // .queryBalances.getQueryEthereumHexAddress(senderConfig.sender) - // : queriesStore - // .get(feeConfig.chainId) - // .queryBalances.getQueryBech32Address(senderConfig.sender); - // - // const currentFeeCurrency = feeConfig.fees[0].currency; - // const currentFeeCurrencyBal = - // queryBalances.getBalanceFromCurrency(currentFeeCurrency); - // - // const currentFee = feeConfig.getFeeTypePrettyForFeeCurrency( - // currentFeeCurrency, - // feeConfig.type - // ); - // if (currentFeeCurrencyBal.toDec().lt(currentFee.toDec())) { - // const isOsmosis = - // chainStore.hasChain(feeConfig.chainId) && - // chainStore.getChain(feeConfig.chainId).hasFeature("osmosis-txfees"); - // - // // Not enough balances for fee. - // // Try to find other fee currency to send. - // for (const feeCurrency of feeConfig.selectableFeeCurrencies) { - // const feeCurrencyBal = - // queryBalances.getBalanceFromCurrency(feeCurrency); - // const fee = feeConfig.getFeeTypePrettyForFeeCurrency( - // feeCurrency, - // feeConfig.type - // ); - // - // // Osmosis의 경우는 fee의 spot price를 알아야 fee를 계산할 수 있다. - // // 그런데 문제는 이게 쿼리가 필요하기 때문에 비동기적이라 response를 기다려야한다. - // // 어쨋든 스왑에 의해서만 fee 계산이 이루어지기 때문에 fee로 Osmo가 0이였다면 이 로직까지 왔을리가 없고 - // // 어떤 갯수의 Osmo던지 스왑 이후에 fee가 0이 될수는 없기 때문에 - // // 0라면 단순히 response 준비가 안된것이라 확신할 수 있다. - // if (isOsmosis && fee.toDec().lte(new Dec(0))) { - // continue; - // } - // - // if (feeCurrencyBal.toDec().gte(fee.toDec())) { - // feeConfig.setFee({ - // type: feeConfig.type, - // currency: feeCurrency, - // }); - // const uiProperties = feeConfig.uiProperties; - // skip = - // !uiProperties.loadingState && - // uiProperties.error == null && - // uiProperties.warning == null; - // return; - // } - // } - // } - // } - // }); - // - // return () => { - // clearTimeout(timeoutId); - // skip = true; - // disposer(); - // }; - // }, [ - // chainStore, - // disableAutomaticFeeSet, - // feeConfig, - // feeConfig.chainId, - // queriesStore, - // senderConfig.sender, - // ]); + useLayoutEffect(() => { + if (disableAutomaticFeeSet) { + return; + } + + // Require to invoke effect whenever chain is changed, + // even though it is not used in logic. + noop(feeConfig.chainId); + + // Try to find other fee currency if the account doesn't have enough fee to pay. + // This logic can be slightly complex, so use mobx's `autorun`. + // This part fairly different with the approach of react's hook. + let skip = false; + // Try until 500ms to avoid the confusion to user. + const timeoutId = setTimeout(() => { + skip = true; + }, 2000); + + const disposer = autorun(() => { + const modularChainInfo = chainStore.getModularChain(feeConfig.chainId); + if (!skip && "starknet" in modularChainInfo) { + const queryBalances = starknetQueriesStore.get( + feeConfig.chainId + ).queryStarknetERC20Balance; + + const ethCoinMinmalDenom = `erc20:${modularChainInfo.starknet.ethContractAddress}`; + const strkCoinMinmalDenom = `erc20:${modularChainInfo.starknet.strkContractAddress}`; + for (const coinMinimalDenom of [ + ethCoinMinmalDenom, + strkCoinMinmalDenom, + ]) { + const feeCurrencyBal = queryBalances.getBalance( + feeConfig.chainId, + chainStore, + senderConfig.sender, + coinMinimalDenom + )?.balance; + + if (!feeCurrencyBal) { + return; + } + + if (feeCurrencyBal.toDec().gt(new Dec(0))) { + feeConfig.setType( + feeCurrencyBal.currency.coinDenom as "ETH" | "STRK" + ); + const uiProperties = feeConfig.uiProperties; + skip = + !uiProperties.loadingState && + uiProperties.error == null && + uiProperties.warning == null; + return; + } + + continue; + } + } + }); + + return () => { + clearTimeout(timeoutId); + skip = true; + disposer(); + }; + }, [ + chainStore, + disableAutomaticFeeSet, + feeConfig, + feeConfig.chainId, + senderConfig.sender, + starknetQueriesStore, + ]); }; export const FeeControl: FunctionComponent<{ @@ -193,7 +168,7 @@ export const FeeControl: FunctionComponent<{ }) => { const { analyticsStore, - queriesStore, + starknetQueriesStore, priceStore, chainStore, uiConfigStore, @@ -210,7 +185,7 @@ export const FeeControl: FunctionComponent<{ useAutoFeeCurrencySelectionOnInit( chainStore, - queriesStore, + starknetQueriesStore, senderConfig, feeConfig, disableAutomaticFeeSet @@ -515,3 +490,7 @@ export const FeeControl: FunctionComponent<{ ); } ); + +const noop = (..._args: any[]) => { + // noop +}; diff --git a/apps/extension/src/pages/starknet/send/index.tsx b/apps/extension/src/pages/starknet/send/index.tsx index 12b2207cc1..054b0f96a5 100644 --- a/apps/extension/src/pages/starknet/send/index.tsx +++ b/apps/extension/src/pages/starknet/send/index.tsx @@ -142,16 +142,15 @@ export const StarknetSendPage: FunctionComponent = observer(() => { const account = accountStore.getAccount(chainId); const starknetAccount = starknetAccountStore.getAccount(chainId); + const starknetQueries = starknetQueriesStore.get(chainId); const sender = account.starknetHexAddress; - const balance = starknetQueriesStore - .get(chainId) - .queryStarknetERC20Balance.getBalance( - chainId, - chainStore, - sender, - currency.coinMinimalDenom - ); + const balance = starknetQueries.queryStarknetERC20Balance.getBalance( + chainId, + chainStore, + sender, + currency.coinMinimalDenom + ); const sendConfigs = useSendTxConfig( chainStore, @@ -313,9 +312,7 @@ export const StarknetSendPage: FunctionComponent = observer(() => { const [isAccountActivationModalOpen, setIsAccountActivationModalOpen] = useState(false); useEffect(() => { - if (isAccountNotDeployed) { - setIsAccountActivationModalOpen(true); - } + setIsAccountActivationModalOpen(isAccountNotDeployed); }, [isAccountNotDeployed]); return ( @@ -433,6 +430,28 @@ export const StarknetSendPage: FunctionComponent = observer(() => { new SubmitStarknetTxHashMsg(chainId, txHash) ) .then(() => { + starknetQueries.queryStarknetERC20Balance + .getBalance( + chainId, + chainStore, + account.starknetHexAddress, + sendConfigs.amountConfig.amount[0].currency.coinMinimalDenom + ) + ?.fetch(); + if ( + sendConfigs.feeConfig.fee && + sendConfigs.feeConfig.fee.currency.coinMinimalDenom !== + sendConfigs.amountConfig.amount[0].currency.coinMinimalDenom + ) { + starknetQueries.queryStarknetERC20Balance + .getBalance( + chainId, + chainStore, + account.starknetHexAddress, + sendConfigs.feeConfig.fee.currency.coinMinimalDenom + ) + ?.fetch(); + } notification.show( "success", intl.formatMessage({ diff --git a/packages/stores-starknet/src/account/base.ts b/packages/stores-starknet/src/account/base.ts index ee4c4c59cd..ae185042af 100644 --- a/packages/stores-starknet/src/account/base.ts +++ b/packages/stores-starknet/src/account/base.ts @@ -29,6 +29,11 @@ export class StarknetAccountBase { return this._isSendingTx; } + @action + setIsDeployingAccount(value: boolean) { + this._isDeployingAccount = value; + } + get isDeployingAccount(): boolean { return this._isDeployingAccount; } @@ -91,7 +96,6 @@ export class StarknetAccountBase { ); try { - this._isDeployingAccount = true; const res = await walletAccount.deployAccount( { classHash, @@ -103,10 +107,8 @@ export class StarknetAccountBase { } ); - this._isDeployingAccount = false; onFulfilled?.(res); } catch (e) { - this._isDeployingAccount = false; onBroadcastFailed?.(e); } } @@ -140,7 +142,6 @@ export class StarknetAccountBase { ); try { - this._isDeployingAccount = true; const res = await walletAccount.deployAccountWithFee( { classHash, @@ -150,22 +151,8 @@ export class StarknetAccountBase { fee ); - walletAccount - .waitForTransaction(res.transaction_hash, { - retryInterval: 1000, - }) - .catch((e) => { - // 오류 처리는 무시한다. - console.log(e); - }) - .finally(() => { - this._isDeployingAccount = false; - }); - return res; } catch (e) { - this._isDeployingAccount = false; - throw e; } }