diff --git a/apps/extension/package.json b/apps/extension/package.json index 02a04f0109..6d292e7016 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -34,6 +34,7 @@ "@keplr-wallet/crypto": "0.12.132", "@keplr-wallet/hooks": "0.12.132", "@keplr-wallet/hooks-internal": "0.12.132", + "@keplr-wallet/hooks-starknet": "0.12.132", "@keplr-wallet/ledger-cosmos": "0.12.132", "@keplr-wallet/popup": "0.12.132", "@keplr-wallet/proto-types": "0.12.132", @@ -47,6 +48,7 @@ "@keplr-wallet/stores-eth": "0.12.132", "@keplr-wallet/stores-ibc": "0.12.132", "@keplr-wallet/stores-internal": "0.12.132", + "@keplr-wallet/stores-starknet": "0.12.132", "@keplr-wallet/types": "0.12.132", "@keplr-wallet/unit": "0.12.132", "@keystonehq/animated-qr": "^0.8.6", @@ -142,6 +144,7 @@ "mobx-react-lite": "^3", "mobx-utils": "^6", "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "react-dom": "^16.8.0 || ^17 || ^18", + "starknet": "^6" } } diff --git a/apps/extension/src/components/contract-address-book-modal/index.tsx b/apps/extension/src/components/contract-address-book-modal/index.tsx index c953184910..69d2e37e8b 100644 --- a/apps/extension/src/components/contract-address-book-modal/index.tsx +++ b/apps/extension/src/components/contract-address-book-modal/index.tsx @@ -46,10 +46,13 @@ export const ContractAddressBookModal: FunctionComponent<{ onSelect: (address: string) => void; close: () => void; }> = observer(({ isOpen, chainId, onSelect, close }) => { - const { queriesStore } = useStore(); + const { chainStore, queriesStore, starknetQueriesStore } = useStore(); const contracts = - queriesStore.get(chainId).tokenContracts.queryTokenContracts.tokenContracts; + "cosmos" in chainStore.getModularChain(chainId) + ? queriesStore.get(chainId).tokenContracts.queryTokenContracts + .tokenContracts + : starknetQueriesStore.get(chainId).queryTokenContracts.tokenContracts; const [search, setSearch] = useState(""); const searchRef = useFocusOnMount(); diff --git a/apps/extension/src/components/dropdown/dropdown.tsx b/apps/extension/src/components/dropdown/dropdown.tsx index 61ddde72fc..b39300d6ab 100644 --- a/apps/extension/src/components/dropdown/dropdown.tsx +++ b/apps/extension/src/components/dropdown/dropdown.tsx @@ -20,6 +20,7 @@ export const Dropdown: FunctionComponent = ({ menuContainerMaxHeight, allowSearch, searchExcludedKeys, + direction = "down", }) => { const [isOpen, setIsOpen] = React.useState(false); const wrapperRef = useRef(null); @@ -124,24 +125,36 @@ export const Dropdown: FunctionComponent = ({ - + + + - 0}> + 0} + direction={direction} + size={size} + > - {filteredItems.map((item) => ( - { - onSelect(item.key); - setIsOpen(false); - }} - > - {item.label} - - ))} + + {filteredItems.map((item) => ( + { + onSelect(item.key); + setIsOpen(false); + }} + > + {item.label} + + ))} + diff --git a/apps/extension/src/components/dropdown/styles.ts b/apps/extension/src/components/dropdown/styles.ts index e1e8774c45..528cc5317b 100644 --- a/apps/extension/src/components/dropdown/styles.ts +++ b/apps/extension/src/components/dropdown/styles.ts @@ -9,6 +9,15 @@ export const Styles = { position: relative; `, + DropdownContainer: styled.div<{ + direction: "up" | "down"; + }>` + display: flex; + flex-direction: ${(props) => + props.direction === "down" ? "column" : "column-reverse"}; + position: relative; + `, + SelectedContainer: styled.div<{ isOpen: boolean; size: string; @@ -68,6 +77,8 @@ export const Styles = { `, MenuContainer: styled.div.withConfig<{ isOpen: boolean; + direction: "up" | "down"; + size: string; }>({ shouldForwardProp: (prop) => { if (prop === "isOpen") { @@ -77,10 +88,19 @@ export const Styles = { }, })` position: absolute; + ${({ direction, size }) => + direction === "down" + ? "" + : size === "small" + ? "bottom: 2.5rem;" + : "bottom: 3.25rem;"} width: 100%; - margin-top: 0.375rem; + ${({ direction }) => + direction === "down" + ? "margin-top: 0.375rem" + : "margin-bottom: 0.375rem"}; z-index: 1; @@ -115,6 +135,13 @@ export const Styles = { } }}; `, + + MenuItemsContainer: styled.div<{ direction: "up" | "down" }>` + display: flex; + flex-direction: ${({ direction }) => + direction === "down" ? "column" : "column-reverse"}; + `, + MenuContainerScroll: styled(SimpleBar).withConfig<{ menuContainerMaxHeight?: string; }>({ diff --git a/apps/extension/src/components/dropdown/types.ts b/apps/extension/src/components/dropdown/types.ts index 6931e1d37f..1399e6d381 100644 --- a/apps/extension/src/components/dropdown/types.ts +++ b/apps/extension/src/components/dropdown/types.ts @@ -19,4 +19,6 @@ export interface DropdownProps { allowSearch?: boolean; searchExcludedKeys?: string[]; + + direction?: "up" | "down"; } diff --git a/apps/extension/src/components/image/index.tsx b/apps/extension/src/components/image/index.tsx index 9dd74199ce..1f82482822 100644 --- a/apps/extension/src/components/image/index.tsx +++ b/apps/extension/src/components/image/index.tsx @@ -1,7 +1,8 @@ import React, { FunctionComponent, useLayoutEffect, useState } from "react"; -import { AppCurrency, ChainInfo } from "@keplr-wallet/types"; +import { AppCurrency, ChainInfo, ModularChainInfo } from "@keplr-wallet/types"; import { observer } from "mobx-react-lite"; import { useStore } from "../../stores"; +import { ChainIdHelper } from "@keplr-wallet/cosmos"; /** * 그냥 이미지 컴포넌트인데 오류 났을때 대체 이미지를 보여주는 기능이 있음 @@ -71,7 +72,7 @@ export const RawImageFallback: FunctionComponent< export const ChainImageFallback: FunctionComponent< Omit, "src" | "alt"> & { - chainInfo: ChainInfo; + chainInfo: ChainInfo | ModularChainInfo; size: string; alt?: string; @@ -105,7 +106,7 @@ export const ChainImageFallback: FunctionComponent< export const CurrencyImageFallback: FunctionComponent< Omit, "src" | "alt"> & { - chainInfo: ChainInfo; + chainInfo: ChainInfo | ModularChainInfo; currency: AppCurrency; size: string; @@ -167,7 +168,7 @@ export const CurrencyImageFallback: FunctionComponent< } } else { if ( - chainStore.getChain(chainInfo.chainId).chainIdentifier === + ChainIdHelper.parse(chainInfo.chainId).identifier === axelarChainIdentifier && currency.coinMinimalDenom !== "uaxl" ) { diff --git a/apps/extension/src/config.ts b/apps/extension/src/config.ts index 5f6b8cf13b..821f7c1299 100644 --- a/apps/extension/src/config.ts +++ b/apps/extension/src/config.ts @@ -1,7 +1,7 @@ import { Bech32Address } from "@keplr-wallet/cosmos"; -import { ChainInfo } from "@keplr-wallet/types"; +import { ChainInfo, ModularChainInfo } from "@keplr-wallet/types"; -export const EmbedChainInfos: ChainInfo[] = [ +export const EmbedChainInfos: (ChainInfo | ModularChainInfo)[] = [ { rpc: "https://rpc-cosmoshub.keplr.app", rest: "https://lcd-cosmoshub.keplr.app", @@ -2414,6 +2414,84 @@ export const EmbedChainInfos: ChainInfo[] = [ ], features: [], }, + { + chainId: "starknet:SN_MAIN", + chainName: "Starknet", + chainSymbolImageUrl: + "https://keplr-ext-update-note-images.s3.amazonaws.com/token/starknet.png", + starknet: { + chainId: "starknet:SN_MAIN", + rpc: "https://rpc-starknet.keplr.app", + currencies: [ + { + type: "erc20", + contractAddress: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + coinDenom: "ETH", + coinMinimalDenom: + "erc20:0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + coinDecimals: 18, + coinGeckoId: "ethereum", + coinImageUrl: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/eip155:1/ethereum-native.png", + }, + { + type: "erc20", + contractAddress: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + coinDenom: "STRK", + coinMinimalDenom: + "erc20:0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + coinDecimals: 18, + coinGeckoId: "starknet", + coinImageUrl: + "https://keplr-ext-update-note-images.s3.amazonaws.com/token/starknet.png", + }, + ], + ethContractAddress: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + strkContractAddress: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + }, + }, + { + chainId: "starknet:SN_SEPOLIA", + chainName: "Starknet Sepolia", + chainSymbolImageUrl: + "https://keplr-ext-update-note-images.s3.amazonaws.com/token/starknet.png", + starknet: { + chainId: "starknet:SN_SEPOLIA", + rpc: "https://rpc-starknet-sepolia.keplr.app", + currencies: [ + { + type: "erc20", + contractAddress: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + coinDenom: "ETH", + coinMinimalDenom: + "erc20:0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + coinDecimals: 18, + coinImageUrl: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/eip155:1/ethereum-native.png", + }, + { + type: "erc20", + contractAddress: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + coinDenom: "STRK", + coinMinimalDenom: + "erc20:0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + coinDecimals: 18, + coinImageUrl: + "https://keplr-ext-update-note-images.s3.amazonaws.com/token/starknet.png", + }, + ], + ethContractAddress: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + strkContractAddress: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + }, + }, ]; // The origins that are able to pass any permission that external webpages can have. diff --git a/apps/extension/src/content-scripts/events.ts b/apps/extension/src/content-scripts/events.ts index 14b53160df..37c2a25bb0 100644 --- a/apps/extension/src/content-scripts/events.ts +++ b/apps/extension/src/content-scripts/events.ts @@ -51,6 +51,19 @@ export function initEvents(router: Router) { }, }) ); + case "keplr_starknetChainChanged": + return window.dispatchEvent( + new CustomEvent("keplr_starknetChainChanged", { + detail: { + ...( + msg as PushEventDataMsg<{ + origin: string; + starknetChainId: string; + }> + ).data.data, + }, + }) + ); case "keplr_ethSubscription": return window.dispatchEvent( new CustomEvent("keplr_ethSubscription", { diff --git a/apps/extension/src/content-scripts/inject/injected-script.ts b/apps/extension/src/content-scripts/inject/injected-script.ts index 83d7c535c0..92534415f8 100644 --- a/apps/extension/src/content-scripts/inject/injected-script.ts +++ b/apps/extension/src/content-scripts/inject/injected-script.ts @@ -1,18 +1,71 @@ import { InjectedKeplr } from "@keplr-wallet/provider"; import { injectKeplrToWindow } from "@keplr-wallet/provider"; +import { RpcProvider, WalletAccount } from "starknet"; import manifest from "../../manifest.v2.json"; const keplr = new InjectedKeplr( manifest.version, "extension", - undefined, + (state) => { + // XXX: RpcProvider와 Account를 starknetjs에서 바로 가져와서 씀으로 인해서 + // injected script의 크기가 커지는 문제가 있다. + // 일단 webpack의 tree shaking 덕분에 아직은 어느정도 허용할만한 수준의 용량이다. + // 이 코드에 의한 용량 문제에 대해서 고려해서 개발해야한다. + if (state.rpc) { + if (!keplr.starknet.provider) { + keplr.starknet.provider = new RpcProvider({ + nodeUrl: state.rpc, + }); + } else { + keplr.starknet.provider.channel.nodeUrl = state.rpc; + } + } + + if (keplr.starknet.provider) { + if (state.selectedAddress) { + if (!keplr.starknet.account) { + keplr.starknet.account = new WalletAccount( + keplr.starknet.provider, + keplr.generateStarknetProvider() + ); + keplr.starknet.account.address = state.selectedAddress; + } else { + keplr.starknet.account.address = state.selectedAddress; + } + } else { + keplr.starknet.account = undefined; + } + } else { + keplr.starknet.account = undefined; + } + }, + (state) => { + if (state.selectedAddress) { + if (keplr.starknet.account) { + keplr.starknet.account.address = state.selectedAddress; + } + } + }, + { + addMessageListener: (fn: (e: any) => void) => + window.addEventListener("message", fn), + removeMessageListener: (fn: (e: any) => void) => + window.removeEventListener("message", fn), + postMessage: (message) => + window.postMessage(message, window.location.origin), + }, undefined, { uuid: crypto.randomUUID(), name: process.env.KEPLR_EXT_EIP6963_PROVIDER_INFO_NAME, icon: process.env.KEPLR_EXT_EIP6963_PROVIDER_INFO_ICON, rdns: process.env.KEPLR_EXT_EIP6963_PROVIDER_INFO_RDNS, + }, + { + id: "braavos", + name: "Braavos", + icon: process.env.KEPLR_EXT_STARKNET_PROVIDER_INFO_ICON, } ); injectKeplrToWindow(keplr); diff --git a/apps/extension/src/env.d.ts b/apps/extension/src/env.d.ts index 76ae806f98..65dce250a2 100644 --- a/apps/extension/src/env.d.ts +++ b/apps/extension/src/env.d.ts @@ -8,5 +8,8 @@ declare namespace NodeJS { KEPLR_EXT_EIP6963_PROVIDER_INFO_NAME: string; KEPLR_EXT_EIP6963_PROVIDER_INFO_RDNS: string; KEPLR_EXT_EIP6963_PROVIDER_INFO_ICON: string; + KEPLR_EXT_STARKNET_PROVIDER_INFO_ID: string; + KEPLR_EXT_STARKNET_PROVIDER_INFO_NAME: string; + KEPLR_EXT_STARKNET_PROVIDER_INFO_ICON: string; } } diff --git a/apps/extension/src/hooks/starknet/use-tx-configs-query-string.ts b/apps/extension/src/hooks/starknet/use-tx-configs-query-string.ts new file mode 100644 index 0000000000..50b87d84dc --- /dev/null +++ b/apps/extension/src/hooks/starknet/use-tx-configs-query-string.ts @@ -0,0 +1,123 @@ +import { + IAmountConfig, + IFeeConfig, + IGasConfig, + IGasSimulator, + IRecipientConfig, +} from "@keplr-wallet/hooks-starknet"; +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; + +export const useStarknetTxConfigsQueryString = (configs: { + amountConfig: IAmountConfig; + recipientConfig?: IRecipientConfig; + feeConfig: IFeeConfig; + gasConfig: IGasConfig; + gasSimulator: IGasSimulator; +}) => { + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + const initialAmountFraction = searchParams.get("initialAmountFraction"); + if ( + initialAmountFraction && + !Number.isNaN(parseFloat(initialAmountFraction)) + ) { + configs.amountConfig.setFraction( + Number.parseFloat(initialAmountFraction) + ); + } + const initialAmount = searchParams.get("initialAmount"); + if (initialAmount) { + // AmountInput에는 price based 모드가 있다. + // 하지만 이 state는 AmountInput Component에서 다뤄지므로 여기서 처리하기가 힘들다. + // 어쨋든 처음에는 non price mode로 시작히므로 이렇게 해도 큰 문제는 없다. + // TODO: 나중에 해결한다. + configs.amountConfig.setValue(initialAmount); + } + const initialRecipient = searchParams.get("initialRecipient"); + if (initialRecipient) { + configs.recipientConfig?.setValue(initialRecipient); + } + + const initialFeeCoinDenom = searchParams.get("initialFeeCoinDenom") as + | "ETH" + | "STRK"; + if (initialFeeCoinDenom) { + configs.feeConfig.setType(initialFeeCoinDenom); + } + + const initialGasAmount = searchParams.get("initialGasAmount"); + if (initialGasAmount) { + configs.gasConfig.setValue(initialGasAmount); + configs.gasSimulator.setEnabled(false); + } else { + const initialGasAdjustment = searchParams.get("initialGasAdjustment"); + if (initialGasAdjustment) { + configs.gasSimulator.setGasAdjustmentValue(initialGasAdjustment); + configs.gasSimulator.setEnabled(true); + } + } + }, []); + + useEffect(() => { + setSearchParams( + (prev) => { + if ( + configs.recipientConfig && + configs.recipientConfig.value.trim().length > 0 + ) { + prev.set("initialRecipient", configs.recipientConfig.value); + } else { + prev.delete("initialRecipient"); + } + // Fraction and amount value are exclusive + if (configs.amountConfig.fraction <= 0) { + prev.delete("initialAmountFraction"); + if (configs.amountConfig.value.trim().length > 0) { + prev.set("initialAmount", configs.amountConfig.value); + } else { + prev.delete("initialAmount"); + } + } else { + prev.delete("initialAmount"); + prev.set( + "initialAmountFraction", + configs.amountConfig.fraction.toString() + ); + } + if (configs.feeConfig.fee != null) { + prev.set( + "initialFeeCoinDenom", + configs.feeConfig.fee?.currency.coinDenom + ); + } + + if (configs.gasSimulator.enabled) { + prev.set( + "initialGasAdjustment", + configs.gasSimulator.gasAdjustment.toString() + ); + prev.delete("initialGasAmount"); + } else { + prev.set("initialGasAmount", configs.gasConfig.value.toString()); + prev.delete("initialGasAdjustment"); + } + return prev; + }, + { + replace: true, + } + ); + }, [ + configs.amountConfig.fraction, + configs.amountConfig.value, + configs.feeConfig.fee, + configs.feeConfig.type, + configs.gasConfig.value, + configs.gasSimulator.enabled, + configs.gasSimulator.gasAdjustment, + configs.recipientConfig, + setSearchParams, + ]); +}; diff --git a/apps/extension/src/index.tsx b/apps/extension/src/index.tsx index 521d1e9ec1..a97005a158 100644 --- a/apps/extension/src/index.tsx +++ b/apps/extension/src/index.tsx @@ -99,6 +99,8 @@ import { RoutePageAnalytics } from "./route-page-analytics"; import { useIntl } from "react-intl"; import { ActivitiesPage } from "./pages/activities"; import { isRunningInSidePanel } from "./utils"; +import { StarknetSendPage } from "./pages/starknet/send"; +import { SignStarknetTxPage } from "./pages/starknet/sign/tx"; configure({ enforceActions: "always", // Make mobx to strict mode. @@ -169,8 +171,8 @@ const RoutesAfterReady: FunctionComponent = observer(() => { // XXX: Below logic not observe state changes on account store and it's inner state. // This is intended because this logic is only for the first time and avoid global re-rendering. // Start init for registered chains so that users can see account address more quickly. - for (const chainInfo of chainStore.chainInfos) { - const account = accountStore.getAccount(chainInfo.chainId); + for (const modularChainInfo of chainStore.modularChainInfos) { + const account = accountStore.getAccount(modularChainInfo.chainId); // Because {autoInit: true} is given as the option on account store, // initialization for the account starts at this time just by using getAccount(). // However, run safe check on current status and init if status is not inited. @@ -374,6 +376,7 @@ const RoutesAfterReady: FunctionComponent = observer(() => { element={} /> } /> + } /> } /> { element={} /> } /> + } + /> } /> } /> { + if (!("currencies" in token.chainInfo)) { + return false; + } + const map = destinationMap.get(token.chainInfo.chainIdentifier); if (map) { return ( @@ -74,10 +78,12 @@ class IBCSwapDestinationState { const tokensKeyMap = new Map(); for (const token of tokens) { - tokensKeyMap.set( - `${token.chainInfo.chainIdentifier}/${token.token.currency.coinMinimalDenom}`, - true - ); + if ("currencies" in token.chainInfo) { + tokensKeyMap.set( + `${token.chainInfo.chainIdentifier}/${token.token.currency.coinMinimalDenom}`, + true + ); + } } for (const [chainIdentifier, map] of destinationMap) { @@ -155,6 +161,10 @@ export const IBCSwapDestinationSelectAssetPage: FunctionComponent = observer( const filteredTokens = useMemo(() => { const filtered = tokens.filter((token) => { + if (!("currencies" in token.chainInfo)) { + return false; + } + return ( !excludeKey || `${token.chainInfo.chainIdentifier}/${token.token.currency.coinMinimalDenom}` !== diff --git a/apps/extension/src/pages/main/components/deposit-modal/copy-address-scene.tsx b/apps/extension/src/pages/main/components/deposit-modal/copy-address-scene.tsx index 7535f35d85..65675851a8 100644 --- a/apps/extension/src/pages/main/components/deposit-modal/copy-address-scene.tsx +++ b/apps/extension/src/pages/main/components/deposit-modal/copy-address-scene.tsx @@ -28,7 +28,7 @@ import { useSceneEvents, useSceneTransition, } from "../../../../components/transition"; -import { IChainInfoImpl } from "@keplr-wallet/stores"; +import { ModularChainInfo } from "@keplr-wallet/types"; export const CopyAddressScene: FunctionComponent<{ close: () => void; @@ -71,36 +71,45 @@ export const CopyAddressScene: FunctionComponent<{ return {}; } const res: Record = {}; - for (const chainInfo of chainStore.chainInfosInUI) { + for (const modularChainInfo of chainStore.modularChainInfosInUI) { if ( uiConfigStore.copyAddressConfig.isBookmarkedChain( keyRingStore.selectedKeyInfo.id, - chainInfo.chainId + modularChainInfo.chainId ) ) { - res[chainInfo.chainIdentifier] = true; + res[ChainIdHelper.parse(modularChainInfo.chainId).identifier] = true; } } return res; }); const addresses: { - chainInfo: IChainInfoImpl; + modularChainInfo: ModularChainInfo; bech32Address?: string; ethereumAddress?: string; - }[] = chainStore.chainInfosInUI - .map((chainInfo) => { - const accountInfo = accountStore.getAccount(chainInfo.chainId); + starknetAddress?: string; + }[] = chainStore.modularChainInfosInUI + .map((modularChainInfo) => { + const accountInfo = accountStore.getAccount(modularChainInfo.chainId); const bech32Address = (() => { - if (chainInfo.chainId.startsWith("eip155")) { + if (!("cosmos" in modularChainInfo)) { + return undefined; + } + + if (modularChainInfo.chainId.startsWith("eip155")) { return undefined; } return accountInfo.bech32Address; })(); const ethereumAddress = (() => { - if (chainInfo.chainId.startsWith("injective")) { + if (!("cosmos" in modularChainInfo)) { + return undefined; + } + + if (modularChainInfo.chainId.startsWith("injective")) { return undefined; } @@ -108,28 +117,37 @@ export const CopyAddressScene: FunctionComponent<{ ? accountInfo.ethereumHexAddress : undefined; })(); + const starknetAddress = (() => { + if (!("starknet" in modularChainInfo)) { + return undefined; + } + + return accountInfo.starknetHexAddress; + })(); return { - chainInfo, + modularChainInfo, bech32Address, ethereumAddress, + starknetAddress, }; }) - .filter((address) => { + .filter(({ modularChainInfo, bech32Address }) => { const s = search.trim().toLowerCase(); if (s.length === 0) { return true; } - if (address.chainInfo.chainId.toLowerCase().includes(s)) { + if (modularChainInfo.chainId.toLowerCase().includes(s)) { return true; } - if (address.chainInfo.chainName.toLowerCase().includes(s)) { + + if (modularChainInfo.chainName.toLowerCase().includes(s)) { return true; } - if (address.bech32Address) { - const bech32Split = address.bech32Address.split("1"); + if (bech32Address) { + const bech32Split = bech32Address.split("1"); if (bech32Split.length > 0) { if (bech32Split[0].toLowerCase().includes(s)) { return true; @@ -137,16 +155,30 @@ export const CopyAddressScene: FunctionComponent<{ } } - if (address.chainInfo.stakeCurrency) { - if ( - address.chainInfo.stakeCurrency.coinDenom.toLowerCase().includes(s) - ) { - return true; + if ("cosmos" in modularChainInfo && modularChainInfo.cosmos != null) { + const cosmosChainInfo = modularChainInfo.cosmos; + if (cosmosChainInfo.stakeCurrency) { + if ( + cosmosChainInfo.stakeCurrency.coinDenom.toLowerCase().includes(s) + ) { + return true; + } } - } - if (address.chainInfo.currencies.length > 0) { - const currency = address.chainInfo.currencies[0]; - if (!currency.coinMinimalDenom.startsWith("ibc/")) { + if (cosmosChainInfo.currencies.length > 0) { + const currency = cosmosChainInfo.currencies[0]; + if (!currency.coinMinimalDenom.startsWith("ibc/")) { + if (currency.coinDenom.toLowerCase().includes(s)) { + return true; + } + } + } + } else if ( + "starknet" in modularChainInfo && + modularChainInfo.starknet != null + ) { + const starknetChainInfo = modularChainInfo.starknet; + if (starknetChainInfo.currencies.length > 0) { + const currency = starknetChainInfo.currencies[0]; if (currency.coinDenom.toLowerCase().includes(s)) { return true; } @@ -154,8 +186,15 @@ export const CopyAddressScene: FunctionComponent<{ } }) .sort((a, b) => { - const aPriority = sortPriorities[a.chainInfo.chainIdentifier]; - const bPriority = sortPriorities[b.chainInfo.chainIdentifier]; + const aChainIdentifier = ChainIdHelper.parse( + a.modularChainInfo.chainId + ).identifier; + const bChainIdentifier = ChainIdHelper.parse( + b.modularChainInfo.chainId + ).identifier; + + const aPriority = sortPriorities[aChainIdentifier]; + const bPriority = sortPriorities[bChainIdentifier]; if (aPriority && bPriority) { return 0; @@ -284,7 +323,7 @@ export const CopyAddressScene: FunctionComponent<{ if (address.ethereumAddress && address.bech32Address) { return [ { - chainInfo: address.chainInfo, + modularChainInfo: address.modularChainInfo, bech32Address: address.bech32Address, }, { @@ -300,7 +339,8 @@ export const CopyAddressScene: FunctionComponent<{ return ( void; @@ -353,14 +394,17 @@ const CopyAddressItem: FunctionComponent<{ const isBookmarked = keyRingStore.selectedKeyInfo ? uiConfigStore.copyAddressConfig.isBookmarkedChain( keyRingStore.selectedKeyInfo.id, - address.chainInfo.chainId + address.modularChainInfo.chainId ) : false; const [isCopyContainerHover, setIsCopyContainerHover] = useState(false); const [isBookmarkHover, setIsBookmarkHover] = useState(false); - const isEVMOnlyChain = chainStore.isEvmOnlyChain(address.chainInfo.chainId); + const isEVMOnlyChain = + "cosmos" in address.modularChainInfo && + address.modularChainInfo.cosmos != null && + chainStore.isEvmOnlyChain(address.modularChainInfo.chainId); // 클릭 영역 문제로 레이아웃이 복잡해졌다. // 알아서 잘 해결하자 @@ -400,14 +444,17 @@ const CopyAddressItem: FunctionComponent<{ e.preventDefault(); await navigator.clipboard.writeText( - address.ethereumAddress || address.bech32Address || "" + address.starknetAddress || + address.ethereumAddress || + address.bech32Address || + "" ); setHasCopied(true); setBlockInteraction(true); analyticsStore.logEvent("click_copyAddress_copy", { - chainId: address.chainInfo.chainId, - chainName: address.chainInfo.chainName, + chainId: address.modularChainInfo.chainId, + chainName: address.modularChainInfo.chainName, }); setHasCopied(true); @@ -466,8 +513,8 @@ const CopyAddressItem: FunctionComponent<{ const newIsBookmarked = !isBookmarked; analyticsStore.logEvent("click_favoriteChain", { - chainId: address.chainInfo.chainId, - chainName: address.chainInfo.chainName, + chainId: address.modularChainInfo.chainId, + chainName: address.modularChainInfo.chainName, isFavorite: newIsBookmarked, }); @@ -475,17 +522,17 @@ const CopyAddressItem: FunctionComponent<{ if (newIsBookmarked) { uiConfigStore.copyAddressConfig.bookmarkChain( keyRingStore.selectedKeyInfo.id, - address.chainInfo.chainId + address.modularChainInfo.chainId ); } else { uiConfigStore.copyAddressConfig.unbookmarkChain( keyRingStore.selectedKeyInfo.id, - address.chainInfo.chainId + address.modularChainInfo.chainId ); setSortPriorities((priorities) => { const identifier = ChainIdHelper.parse( - address.chainInfo.chainId + address.modularChainInfo.chainId ).identifier; const newPriorities = { ...priorities }; if (newPriorities[identifier]) { @@ -501,7 +548,10 @@ const CopyAddressItem: FunctionComponent<{ - + - {address.chainInfo.chainName} + {address.modularChainInfo.chainName} {(() => { + if (address.starknetAddress) { + return `${address.starknetAddress.slice( + 0, + 10 + )}...${address.starknetAddress.slice(-8)}`; + } + if (address.ethereumAddress) { return address.ethereumAddress.length === 42 ? `${address.ethereumAddress.slice( @@ -576,8 +633,11 @@ const CopyAddressItem: FunctionComponent<{ disabled={hasCopied} onClick={() => { sceneTransition.push("qr-code", { - chainId: address.chainInfo.chainId, - address: address.ethereumAddress || address.bech32Address, + chainId: address.modularChainInfo.chainId, + address: + address.starknetAddress || + address.ethereumAddress || + address.bech32Address, }); }} > diff --git a/apps/extension/src/pages/main/components/deposit-modal/qr-code.tsx b/apps/extension/src/pages/main/components/deposit-modal/qr-code.tsx index 25dd931ce2..5d8cca7ac7 100644 --- a/apps/extension/src/pages/main/components/deposit-modal/qr-code.tsx +++ b/apps/extension/src/pages/main/components/deposit-modal/qr-code.tsx @@ -22,7 +22,7 @@ export const QRCodeScene: FunctionComponent<{ const theme = useTheme(); - const chainInfo = chainStore.getChain(chainId); + const modularChainInfo = chainStore.getModularChain(chainId); const sceneTransition = useSceneTransition(); @@ -64,9 +64,9 @@ export const QRCodeScene: FunctionComponent<{ - + - {chainInfo.chainName} + {modularChainInfo.chainName} {/* 체인 아이콘과 이름을 중앙 정렬시키기 위해서 왼쪽과 맞춰야한다. 이를 위한 mock임 */} diff --git a/apps/extension/src/pages/main/components/token-found-modal/index.tsx b/apps/extension/src/pages/main/components/token-found-modal/index.tsx index 21a31bae2c..d8a694dee0 100644 --- a/apps/extension/src/pages/main/components/token-found-modal/index.tsx +++ b/apps/extension/src/pages/main/components/token-found-modal/index.tsx @@ -87,32 +87,43 @@ export const TokenFoundModal: FunctionComponent<{ const tokenScans = chainStore.tokenScans.slice(); for (const enable of enables) { - if ( - keyRingStore.needKeyCoinTypeFinalize( - keyRingStore.selectedKeyInfo.id, - chainStore.getChain(enable) - ) - ) { + const modularChainInfo = chainStore.getModularChain(enable); + if ("cosmos" in modularChainInfo) { + if ( + keyRingStore.needKeyCoinTypeFinalize( + keyRingStore.selectedKeyInfo.id, + chainStore.getChain(enable) + ) + ) { + const tokenScan = tokenScans.find((tokenScan) => { + return ChainIdHelper.parse(tokenScan.chainId).identifier === enable; + }); + + if (tokenScan && tokenScan.infos.length > 1) { + needBIP44Selects.push(enable); + enables.splice(enables.indexOf(enable), 1); + } + + if ( + tokenScan && + tokenScan.infos.length === 1 && + tokenScan.infos[0].coinType != null + ) { + await keyRingStore.finalizeKeyCoinType( + keyRingStore.selectedKeyInfo.id, + enable, + tokenScan.infos[0].coinType + ); + } + } + } else if ("starknet" in modularChainInfo) { const tokenScan = tokenScans.find((tokenScan) => { return ChainIdHelper.parse(tokenScan.chainId).identifier === enable; }); if (tokenScan && tokenScan.infos.length > 1) { - needBIP44Selects.push(enable); enables.splice(enables.indexOf(enable), 1); } - - if ( - tokenScan && - tokenScan.infos.length === 1 && - tokenScan.infos[0].coinType != null - ) { - await keyRingStore.finalizeKeyCoinType( - keyRingStore.selectedKeyInfo.id, - enable, - tokenScan.infos[0].coinType - ); - } } } @@ -318,7 +329,11 @@ const FoundChainView: FunctionComponent<{ @@ -332,7 +347,12 @@ const FoundChainView: FunctionComponent<{ : ColorPalette["gray-10"] } > - {chainStore.getChain(tokenScan.chainId).chainName} + { + (chainStore.hasChain(tokenScan.chainId) + ? chainStore.getChain(tokenScan.chainId) + : chainStore.getModularChain(tokenScan.chainId) + ).chainName + } {numTokens} Tokens @@ -391,7 +411,11 @@ const FoundTokenView: FunctionComponent<{ - { - chainStore - .getChain(chainId) - .forceFindCurrency(asset.currency.coinMinimalDenom).coinDenom - } + {(() => { + if (chainStore.hasChain(chainId)) { + return chainStore + .getChain(chainId) + .forceFindCurrency(asset.currency.coinMinimalDenom).coinDenom; + } else { + const modularChainInfo = chainStore.getModularChain(chainId); + if ("starknet" in modularChainInfo) { + return ( + chainStore + .getModularChainInfoImpl(chainId) + .getCurrencies("starknet") + .find( + (cur) => + cur.coinMinimalDenom === asset.currency.coinMinimalDenom + )?.coinDenom ?? asset.currency.coinDenom + ); + } else if ("cosmos" in modularChainInfo) { + return ( + chainStore + .getModularChainInfoImpl(chainId) + .getCurrencies("cosmos") + .find( + (cur) => + cur.coinMinimalDenom === asset.currency.coinMinimalDenom + )?.coinDenom ?? asset.currency.coinDenom + ); + } else { + return asset.currency.coinDenom; + } + } + })()} @@ -421,20 +472,49 @@ const FoundTokenView: FunctionComponent<{ : ColorPalette["gray-50"] } > - {uiConfigStore.hideStringIfPrivacyMode( - new CoinPretty( - chainStore - .getChain(chainId) - .forceFindCurrency(asset.currency.coinMinimalDenom), - asset.amount - ) - .shrink(true) - .trim(true) - .maxDecimals(6) - .inequalitySymbol(true) - .toString(), - 2 - )} + {(() => { + const currency = (() => { + if (chainStore.hasChain(chainId)) { + return chainStore + .getChain(chainId) + .forceFindCurrency(asset.currency.coinMinimalDenom); + } else { + const modularChainInfo = chainStore.getModularChain(chainId); + if ("starknet" in modularChainInfo) { + return ( + chainStore + .getModularChainInfoImpl(chainId) + .getCurrencies("starknet") + .find( + (cur) => + cur.coinMinimalDenom === asset.currency.coinMinimalDenom + ) ?? asset.currency + ); + } else if ("cosmos" in modularChainInfo) { + return ( + chainStore + .getModularChainInfoImpl(chainId) + .getCurrencies("cosmos") + .find( + (cur) => + cur.coinMinimalDenom === asset.currency.coinMinimalDenom + ) ?? asset.currency + ); + } else { + return asset.currency; + } + } + })(); + return uiConfigStore.hideStringIfPrivacyMode( + new CoinPretty(currency, asset.amount) + .shrink(true) + .trim(true) + .maxDecimals(6) + .inequalitySymbol(true) + .toString(), + 2 + ); + })()} ); diff --git a/apps/extension/src/pages/main/index.tsx b/apps/extension/src/pages/main/index.tsx index 289526f8b5..08d016737e 100644 --- a/apps/extension/src/pages/main/index.tsx +++ b/apps/extension/src/pages/main/index.tsx @@ -57,10 +57,11 @@ import { useBuy } from "../../hooks/use-buy"; import { BottomTabsHeightRem } from "../../bottom-tabs"; import { DenomHelper } from "@keplr-wallet/common"; import { NewSidePanelHeaderTop } from "./new-side-panel-header-top"; +import { ModularChainInfo } from "@keplr-wallet/types"; export interface ViewToken { token: CoinPretty; - chainInfo: IChainInfoImpl; + chainInfo: IChainInfoImpl | ModularChainInfo; isFetching: boolean; error: QueryError | undefined; } @@ -115,6 +116,10 @@ export const MainPage: FunctionComponent<{ const availableTotalPriceEmbedOnlyUSD = useMemo(() => { let result: PricePretty | undefined; for (const bal of hugeQueriesStore.allKnownBalances) { + // TODO: 이거 starknet에서도 embedded를 확인할 수 있도록 수정해야함. + if (!("currencies" in bal.chainInfo)) { + continue; + } if (!(bal.chainInfo.embedded as ChainInfoWithCoreTypes).embedded) { continue; } @@ -168,6 +173,9 @@ export const MainPage: FunctionComponent<{ const stakedTotalPriceEmbedOnlyUSD = useMemo(() => { let result: PricePretty | undefined; for (const bal of hugeQueriesStore.delegations) { + if (!("currencies" in bal.chainInfo)) { + continue; + } if (!(bal.chainInfo.embedded as ChainInfoWithCoreTypes).embedded) { continue; } @@ -183,6 +191,9 @@ export const MainPage: FunctionComponent<{ } } for (const bal of hugeQueriesStore.unbondings) { + if (!("currencies" in bal.viewToken.chainInfo)) { + continue; + } if ( !(bal.viewToken.chainInfo.embedded as ChainInfoWithCoreTypes).embedded ) { diff --git a/apps/extension/src/pages/main/layouts/header.tsx b/apps/extension/src/pages/main/layouts/header.tsx index 527cb50a94..ed76ad2326 100644 --- a/apps/extension/src/pages/main/layouts/header.tsx +++ b/apps/extension/src/pages/main/layouts/header.tsx @@ -30,13 +30,10 @@ import { BACKGROUND_PORT } from "@keplr-wallet/router"; import { GetCurrentChainIdForEVMMsg, UpdateCurrentChainIdForEVMMsg, + GetCurrentChainIdForStarknetMsg, + UpdateCurrentChainIdForStarknetMsg, } from "@keplr-wallet/background"; -import { - autoPlacement, - autoUpdate, - offset, - useFloating, -} from "@floating-ui/react-dom"; +import { autoUpdate, offset, shift, useFloating } from "@floating-ui/react-dom"; import SimpleBar from "simplebar-react"; import { ExtensionKVStore } from "@keplr-wallet/common"; @@ -93,47 +90,77 @@ export const MainHeaderLayout = observer< const [currentChainIdForEVM, setCurrentChainIdForEVM] = React.useState< string | undefined >(); + const [currentChainIdForStarknet, setCurrentChainIdForStarknet] = + React.useState(); const [activeTabOrigin, setActiveTabOrigin] = React.useState< string | undefined >(); useEffect(() => { - const updateCurrentChainIdForEVM = async () => { + const updateCurrentChainId = async () => { const activeTabOrigin = await getActiveTabOrigin(); if (activeTabOrigin) { - const msg = new GetCurrentChainIdForEVMMsg(activeTabOrigin); + const msgForEVM = new GetCurrentChainIdForEVMMsg(activeTabOrigin); + const msgForStarknet = new GetCurrentChainIdForStarknetMsg( + activeTabOrigin + ); const newCurrentChainIdForEVM = await new InExtensionMessageRequester().sendMessage( BACKGROUND_PORT, - msg + msgForEVM + ); + const newCurrentChainIdForStarknet = + await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msgForStarknet ); setCurrentChainIdForEVM(newCurrentChainIdForEVM); + setCurrentChainIdForStarknet(newCurrentChainIdForStarknet); setActiveTabOrigin(activeTabOrigin); } else { setCurrentChainIdForEVM(undefined); + setCurrentChainIdForStarknet(undefined); setActiveTabOrigin(undefined); } }; - browser.tabs.onActivated.addListener(updateCurrentChainIdForEVM); - updateCurrentChainIdForEVM(); - // Update current chain id for EVM every second. + browser.tabs.onActivated.addListener(updateCurrentChainId); + updateCurrentChainId(); + // Update current chain id for EVM and Starknet every second. // TODO: Make it sync with `chainChanged` event. - const intervalId = setInterval(updateCurrentChainIdForEVM, 1000); + const intervalId = setInterval(updateCurrentChainId, 1000); return () => { - browser.tabs.onActivated.removeListener(updateCurrentChainIdForEVM); + browser.tabs.onActivated.removeListener(updateCurrentChainId); clearInterval(intervalId); }; }, []); - const [isHoveredCurrenctChainIcon, setIsHoveredCurrenctChainIcon] = - React.useState(false); - const [isOpenCurrentChainDropdown, setIsOpenCurrentChainDropdown] = - React.useState(false); + const [ + isOpenCurrentChainSelectorForEVM, + setIsOpenCurrentChainSelectorForEVM, + ] = React.useState(false); + const [ + isHoveredCurrenctChainIconForEVM, + setIsHoveredCurrenctChainIconForEVM, + ] = React.useState(false); + const evmChainInfos = chainStore.chainInfos.filter((chainInfo) => chainStore.isEvmChain(chainInfo.chainId) ); + const [ + isOpenCurrentChainSelectorForStarknet, + setIsOpenCurrentChainSelectorForStarknet, + ] = React.useState(false); + const [ + isHoveredCurrenctChainIconForStarknet, + setIsHoveredCurrenctChainIconForStarknet, + ] = React.useState(false); + + const starknetChainInfos = chainStore.modularChainInfos.filter( + (modularChainInfo) => "starknet" in modularChainInfo + ); + const [isOpenMenu, setIsOpenMenu] = React.useState(false); const [ @@ -340,10 +367,78 @@ export const MainHeaderLayout = observer< } right={ + {currentChainIdForStarknet != null && activeTabOrigin != null && ( + setIsOpenCurrentChainSelectorForStarknet(false)} + items={starknetChainInfos.map((chainInfo) => ({ + key: chainInfo.chainId, + content: ( + + + {chainInfo.chainName} + + ), + onSelect: async (key) => { + const msg = new UpdateCurrentChainIdForStarknetMsg( + activeTabOrigin, + key + ); + await new InExtensionMessageRequester().sendMessage( + BACKGROUND_PORT, + msg + ); + setCurrentChainIdForStarknet(key); + }, + }))} + selectedItemKey={currentChainIdForStarknet} + activeTabOrigin={activeTabOrigin} + isForStarknet={true} + > + setIsOpenCurrentChainSelectorForStarknet(true)} + > + + + + + + + )} {currentChainIdForEVM != null && activeTabOrigin != null && ( - setIsOpenCurrentChainDropdown(false)} + setIsOpenCurrentChainSelectorForEVM(false)} items={evmChainInfos.map((chainInfo) => ({ key: chainInfo.chainId, content: ( @@ -371,13 +466,15 @@ export const MainHeaderLayout = observer< borderRadius="99999px" position="relative" cursor="pointer" - onHoverStateChange={setIsHoveredCurrenctChainIcon} - onClick={() => setIsOpenCurrentChainDropdown(true)} + onHoverStateChange={setIsHoveredCurrenctChainIconForEVM} + onClick={() => setIsOpenCurrentChainSelectorForEVM(true)} > - + )} @@ -431,7 +528,7 @@ export const MainHeaderLayout = observer< } ); -const EVMChainSelector: FunctionComponent< +const ChainSelector: FunctionComponent< PropsWithChildren<{ isOpen: boolean; close: () => void; @@ -442,15 +539,22 @@ const EVMChainSelector: FunctionComponent< }[]; selectedItemKey: string; activeTabOrigin: string; + isForStarknet?: boolean; }> > = observer( - ({ children, isOpen, close, items, selectedItemKey, activeTabOrigin }) => { + ({ + children, + isOpen, + close, + items, + selectedItemKey, + activeTabOrigin, + isForStarknet, + }) => { const { x, y, strategy, refs } = useFloating({ placement: "bottom-end", middleware: [ - autoPlacement({ - allowedPlacements: ["bottom-end"], - }), + shift(), offset({ mainAxis: 10, crossAxis: 10, @@ -580,7 +684,9 @@ const EVMChainSelector: FunctionComponent< }} > - {"EVM compatible chains require users to"} + {`${ + isForStarknet ? "Starknet" : "EVM" + } compatible chains require users to`}
{"manually switch between networks in"}
{"their wallets."} diff --git a/apps/extension/src/pages/main/staked.tsx b/apps/extension/src/pages/main/staked.tsx index 214407e9c7..3d5149f62a 100644 --- a/apps/extension/src/pages/main/staked.tsx +++ b/apps/extension/src/pages/main/staked.tsx @@ -2,7 +2,6 @@ import React, { FunctionComponent, useMemo } from "react"; import { CollapsibleList } from "../../components/collapsible-list"; import { MainEmptyView, TokenItem, TokenTitleView } from "./components"; import { Dec } from "@keplr-wallet/unit"; -import { ViewToken } from "./index"; import { observer } from "mobx-react-lite"; import { Stack } from "../../components/stack"; import { useStore } from "../../stores"; @@ -10,6 +9,7 @@ import { TextButton } from "../../components/button-text"; import { ArrowRightSolidIcon } from "../../components/icon"; import { ColorPalette } from "../../styles"; import { useIntl } from "react-intl"; +import { ViewTokenCosmosOnly } from "../../stores/huge-queries"; export const StakedTabView: FunctionComponent<{ onMoreTokensClosed: () => void; @@ -18,7 +18,7 @@ export const StakedTabView: FunctionComponent<{ const intl = useIntl(); - const delegations: ViewToken[] = useMemo( + const delegations: ViewTokenCosmosOnly[] = useMemo( () => hugeQueriesStore.delegations.filter((token) => { return token.token.toDec().gt(new Dec(0)); @@ -27,7 +27,7 @@ export const StakedTabView: FunctionComponent<{ ); const unbondings: { - viewToken: ViewToken; + viewToken: ViewTokenCosmosOnly; altSentence: string; }[] = useMemo( () => @@ -52,9 +52,9 @@ export const StakedTabView: FunctionComponent<{ const TokenViewData: { title: string; balance: - | ViewToken[] + | ViewTokenCosmosOnly[] | { - viewToken: ViewToken; + viewToken: ViewTokenCosmosOnly; altSentence: string; }[]; lenAlwaysShown: number; diff --git a/apps/extension/src/pages/main/token-detail/address-chip/index.tsx b/apps/extension/src/pages/main/token-detail/address-chip/index.tsx index 0683ec8259..5bb866f78d 100644 --- a/apps/extension/src/pages/main/token-detail/address-chip/index.tsx +++ b/apps/extension/src/pages/main/token-detail/address-chip/index.tsx @@ -19,7 +19,13 @@ export const AddressChip: FunctionComponent<{ }> = observer(({ chainId, inModal }) => { const { accountStore, chainStore } = useStore(); - const isEVMOnlyChain = chainStore.isEvmOnlyChain(chainId); + const modularChainInfo = chainStore.getModularChain(chainId); + const isEVMOnlyChain = (() => { + if ("cosmos" in modularChainInfo) { + return chainStore.isEvmOnlyChain(chainId); + } + return false; + })(); const theme = useTheme(); @@ -73,10 +79,15 @@ export const AddressChip: FunctionComponent<{ onClick={(e) => { e.preventDefault(); - // copy address - navigator.clipboard.writeText( - isEVMOnlyChain ? account.ethereumHexAddress : account.bech32Address - ); + if ("cosmos" in modularChainInfo) { + // copy address + navigator.clipboard.writeText( + isEVMOnlyChain ? account.ethereumHexAddress : account.bech32Address + ); + } else { + // copy address + navigator.clipboard.writeText(account.starknetHexAddress); + } setAnimCheck(true); }} onHoverStateChange={setIsHover} @@ -89,12 +100,20 @@ export const AddressChip: FunctionComponent<{ : ColorPalette["gray-200"] } > - {isEVMOnlyChain - ? `${account.ethereumHexAddress.slice( - 0, - 10 - )}...${account.ethereumHexAddress.slice(32)}` - : Bech32Address.shortenAddress(account.bech32Address, 16)} + {(() => { + if ("cosmos" in modularChainInfo) { + return isEVMOnlyChain + ? `${account.ethereumHexAddress.slice( + 0, + 10 + )}...${account.ethereumHexAddress.slice(32)}` + : Bech32Address.shortenAddress(account.bech32Address, 16); + } + return `${account.starknetHexAddress.slice( + 0, + 10 + )}...${account.starknetHexAddress.slice(56)}`; + })()} {!animCheck ? ( diff --git a/apps/extension/src/pages/main/token-detail/modal.tsx b/apps/extension/src/pages/main/token-detail/modal.tsx index 50077b70d0..db4b738505 100644 --- a/apps/extension/src/pages/main/token-detail/modal.tsx +++ b/apps/extension/src/pages/main/token-detail/modal.tsx @@ -28,6 +28,7 @@ import { MsgItemSkeleton } from "./msg-items/skeleton"; import { Stack } from "../../../components/stack"; import { EmptyView } from "../../../components/empty-view"; import { DenomHelper } from "@keplr-wallet/common"; +import { ChainIdHelper } from "@keplr-wallet/cosmos"; const Styles = { Container: styled.div` @@ -68,6 +69,7 @@ export const TokenDetailModal: FunctionComponent<{ chainStore, accountStore, queriesStore, + starknetQueriesStore, priceStore, price24HChangesStore, skipQueriesStore, @@ -76,13 +78,38 @@ export const TokenDetailModal: FunctionComponent<{ const theme = useTheme(); const account = accountStore.getAccount(chainId); - const chainInfo = chainStore.getChain(chainId); - const currency = chainInfo.forceFindCurrency(coinMinimalDenom); + const modularChainInfo = chainStore.getModularChain(chainId); + const currency = (() => { + if ("cosmos" in modularChainInfo) { + return chainStore.getChain(chainId).forceFindCurrency(coinMinimalDenom); + } + // TODO: 일단 cosmos가 아니면 대충에기에다가 force currency 로직을 박아놓는다... + // 나중에 이런 기능을 chain store 자체에다가 만들어야한다. + const modularChainInfoImpl = chainStore.getModularChainInfoImpl(chainId); + const res = modularChainInfoImpl + .getCurrencies("starknet") + .find((cur) => cur.coinMinimalDenom === coinMinimalDenom); + if (res) { + return res; + } + return { + coinMinimalDenom, + coinDenom: coinMinimalDenom, + coinDecimals: 0, + }; + })(); const denomHelper = new DenomHelper(currency.coinMinimalDenom); const isERC20 = denomHelper.type === "erc20"; - const isMainCurrency = - (chainInfo.stakeCurrency || chainInfo.currencies[0]).coinMinimalDenom === - currency.coinMinimalDenom; + const isMainCurrency = (() => { + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain(chainId); + return ( + (chainInfo.stakeCurrency || chainInfo.currencies[0]) + .coinMinimalDenom === currency.coinMinimalDenom + ); + } + return false; + })(); const isIBCCurrency = "paths" in currency; @@ -93,16 +120,26 @@ export const TokenDetailModal: FunctionComponent<{ const isSomeBuySupport = buySupportServiceInfos.some( (serviceInfo) => !!serviceInfo.buyUrl ); - - const queryBalances = queriesStore.get(chainId).queryBalances; - const balance = - chainStore.isEvmChain(chainId) && (isMainCurrency || isERC20) - ? queryBalances - .getQueryEthereumHexAddress(account.ethereumHexAddress) - .getBalance(currency) - : queryBalances - .getQueryBech32Address(account.bech32Address) - .getBalance(currency); + const balance = (() => { + if ("cosmos" in modularChainInfo) { + const queryBalances = queriesStore.get(chainId).queryBalances; + return chainStore.isEvmChain(chainId) && (isMainCurrency || isERC20) + ? queryBalances + .getQueryEthereumHexAddress(account.ethereumHexAddress) + .getBalance(currency) + : queryBalances + .getQueryBech32Address(account.bech32Address) + .getBalance(currency); + } + return starknetQueriesStore + .get(chainId) + .queryStarknetERC20Balance.getBalance( + chainId, + chainStore, + account.starknetHexAddress, + currency.coinMinimalDenom + ); + })(); const price24HChange = (() => { if (!currency.coinGeckoId) { @@ -119,13 +156,18 @@ export const TokenDetailModal: FunctionComponent<{ ); const isSupported: boolean = useMemo(() => { - const map = new Map(); - for (const chainIdentifier of querySupported.response?.data ?? []) { - map.set(chainIdentifier, true); - } + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain(modularChainInfo.chainId); + const map = new Map(); + for (const chainIdentifier of querySupported.response?.data ?? []) { + map.set(chainIdentifier, true); + } - return map.get(chainInfo.chainIdentifier) ?? false; - }, [chainInfo, querySupported.response]); + return map.get(chainInfo.chainIdentifier) ?? false; + } + // XXX: 어차피 cosmos 기반이 아니면 backend에서 지원하지 않음... + return false; + }, [chainStore, modularChainInfo, querySupported.response]); const buttons: { icon: React.ReactElement; @@ -232,9 +274,16 @@ export const TokenDetailModal: FunctionComponent<{ ), text: "Send", onClick: () => { - navigate( - `/send?chainId=${chainId}&coinMinimalDenom=${coinMinimalDenom}` - ); + if ("cosmos" in modularChainInfo) { + navigate( + `/send?chainId=${chainId}&coinMinimalDenom=${coinMinimalDenom}` + ); + } + if ("starknet" in modularChainInfo) { + navigate( + `/starknet/send?chainId=${chainId}&coinMinimalDenom=${coinMinimalDenom}` + ); + } }, }, ]; @@ -242,9 +291,14 @@ export const TokenDetailModal: FunctionComponent<{ const msgHistory = usePaginatedCursorQuery( process.env["KEPLR_EXT_TX_HISTORY_BASE_URL"], () => { - return `/history/msgs/${chainInfo.chainIdentifier}/${ - accountStore.getAccount(chainId).bech32Address - }?relations=${Relations.join(",")}&denoms=${encodeURIComponent( + return `/history/msgs/${ + ChainIdHelper.parse(chainId).identifier + }/${(() => { + if ("cosmos" in modularChainInfo) { + return accountStore.getAccount(chainId).bech32Address; + } + return accountStore.getAccount(chainId).starknetHexAddress; + })()}?relations=${Relations.join(",")}&denoms=${encodeURIComponent( currency.coinMinimalDenom )}&vsCurrencies=${priceStore.defaultVsCurrency}&limit=${PaginationLimit}`; }, @@ -351,7 +405,7 @@ export const TokenDetailModal: FunctionComponent<{ : ColorPalette["gray-200"] } > - {chainInfo.chainName} + {modularChainInfo.chainName}
@@ -446,14 +500,24 @@ export const TokenDetailModal: FunctionComponent<{ - {chainInfo.stakeCurrency && - chainInfo.stakeCurrency.coinMinimalDenom === - currency.coinMinimalDenom ? ( - - - - - ) : null} + {(() => { + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain(chainId); + if ( + chainInfo.stakeCurrency && + chainInfo.stakeCurrency.coinMinimalDenom === + currency.coinMinimalDenom + ) { + return ( + + + + + ); + } + } + return null; + })()} {(() => { const infos: { @@ -608,7 +672,12 @@ export const TokenDetailModal: FunctionComponent<{ } if (msgHistory.pages[0].response?.isUnsupported || !isSupported) { - if (chainInfo.embedded.embedded) { + // TODO: 아직 cosmos 체인이 아니면 embedded인지 아닌지 구분할 수 없다. + if ( + ("cosmos" in modularChainInfo && + chainStore.getChain(chainId).embedded.embedded) || + "starknet" in modularChainInfo + ) { return ( @@ -629,6 +698,7 @@ export const TokenDetailModal: FunctionComponent<{ ); } + return ( diff --git a/apps/extension/src/pages/main/token-detail/receive-modal/index.tsx b/apps/extension/src/pages/main/token-detail/receive-modal/index.tsx index 8ec06c7e67..64dbb197a8 100644 --- a/apps/extension/src/pages/main/token-detail/receive-modal/index.tsx +++ b/apps/extension/src/pages/main/token-detail/receive-modal/index.tsx @@ -20,9 +20,14 @@ export const ReceiveModal: FunctionComponent<{ const theme = useTheme(); - const chainInfo = chainStore.getChain(chainId); + const modularChainInfo = chainStore.getModularChain(chainId); const account = accountStore.getAccount(chainId); - const isEVMOnlyChain = chainStore.isEvmOnlyChain(chainId); + const isStarknetChain = + "starknet" in modularChainInfo && modularChainInfo.starknet != null; + const isEVMOnlyChain = + "cosmos" in modularChainInfo && + modularChainInfo.cosmos != null && + chainStore.isEvmOnlyChain(chainId); return ( - + - {chainInfo.chainName} + {modularChainInfo.chainName} @@ -65,7 +70,9 @@ export const ReceiveModal: FunctionComponent<{ > = observer(({ data }) => { + const { chainStore, permissionStore } = useStore(); + const intl = useIntl(); + const theme = useTheme(); + + const interactionInfo = useInteractionInfo(); + + const [currentChainIdForStarknet, setCurrentChainIdForStarknet] = + useState(data.chainIds[0]); + + // 페이지가 언마운트 되지 않고 data만 바뀌는 경우가 있어서 이렇게 처리함 + useEffect(() => { + setCurrentChainIdForStarknet(data.chainIds[0]); + }, [data.chainIds]); + + return ( + { + const obsolete = data.ids.find((id) => { + return permissionStore.isObsoleteInteraction(id); + }); + return !!obsolete; + })(), + }} + onSubmit={async (e) => { + e.preventDefault(); + + await permissionStore.approvePermissionWithProceedNext( + data.ids, + (proceedNext) => { + if (!proceedNext) { + if ( + interactionInfo.interaction && + !interactionInfo.interactionInternal + ) { + handleExternalInteractionWithNoProceedNext(); + } + } + }, + currentChainIdForStarknet + ); + }} + > + + + Keplr Logo Image + + + +

+ +

+ + + + + {data.origins.join(", ")} + + + +
+ + {!data.options?.isUnableToChangeChainInUI ? ( + + + Connect + + + chainInfo.chainId.startsWith("starknet:") + ) + .map((chainInfo) => ({ + key: `${chainInfo.chainId}`, + label: chainInfo.chainName, + }))} + onSelect={(chainId) => setCurrentChainIdForStarknet(chainId)} + selectedItemKey={currentChainIdForStarknet} + style={{ padding: "1rem", height: "auto" }} + /> + + ) : ( + + + + {chainStore.getModularChain(data.chainIds[0]).chainName} + + + + + )} + + +
+ ); +}); diff --git a/apps/extension/src/pages/permission/basic-access/index.tsx b/apps/extension/src/pages/permission/basic-access/index.tsx index 13f98996ba..f0a84356b2 100644 --- a/apps/extension/src/pages/permission/basic-access/index.tsx +++ b/apps/extension/src/pages/permission/basic-access/index.tsx @@ -106,7 +106,13 @@ export const PermissionBasicAccessPage: FunctionComponent<{ > {data.chainIds.map((chainId, index) => { - const chainInfo = chainStore.getChain(chainId); + const chainInfo = (() => { + try { + return chainStore.getChain(chainId); + } catch (e) { + return chainStore.getModularChain(chainId); + } + })(); const isLast = index === data.chainIds.length - 1; diff --git a/apps/extension/src/pages/permission/index.tsx b/apps/extension/src/pages/permission/index.tsx index c656e31194..d0386af7c8 100644 --- a/apps/extension/src/pages/permission/index.tsx +++ b/apps/extension/src/pages/permission/index.tsx @@ -8,6 +8,7 @@ import { GlobalPermissionGetChainInfosPage } from "./get-chain-infos"; import { useInteractionInfo } from "../../hooks"; import { FormattedMessage } from "react-intl"; import { PermissionBasicAccessForEVMPage } from "./basic-access-for-evm"; +import { PermissionBasicAccessForStarknetPage } from "./basic-access-for-starknet"; const UnknownPermissionPage: FunctionComponent<{ data: { @@ -47,9 +48,16 @@ export const PermissionPage: FunctionComponent = observer(() => { const mergedData = permissionStore.waitingPermissionMergedData; const mergedDataForEVM = permissionStore.waitingPermissionMergedDataForEVM; + const mergedDataForStarknet = + permissionStore.waitingPermissionMergedDataForStarknet; const globalPermissionData = permissionStore.waitingGlobalPermissionData; - if (!mergedData && !mergedDataForEVM && !globalPermissionData) { + if ( + !mergedData && + !mergedDataForEVM && + !mergedDataForStarknet && + !globalPermissionData + ) { return ; } @@ -75,6 +83,19 @@ export const PermissionPage: FunctionComponent = observer(() => { } } + if (mergedDataForStarknet) { + switch (mergedDataForStarknet.type) { + case "basic-access": { + return ( + + ); + } + default: { + return ; + } + } + } + if (globalPermissionData) { switch (globalPermissionData.data.type) { case "get-chain-infos": { diff --git a/apps/extension/src/pages/register/enable-chains/index.tsx b/apps/extension/src/pages/register/enable-chains/index.tsx index dd3f91ef7d..8e64f5b5f9 100644 --- a/apps/extension/src/pages/register/enable-chains/index.tsx +++ b/apps/extension/src/pages/register/enable-chains/index.tsx @@ -14,7 +14,7 @@ import { useSceneEvents, useSceneTransition, } from "../../../components/transition"; -import { ChainInfo } from "@keplr-wallet/types"; +import { ChainInfo, ModularChainInfo } from "@keplr-wallet/types"; import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { Box } from "../../../components/box"; import { Column, Columns } from "../../../components/column"; @@ -23,6 +23,7 @@ import { Gutter } from "../../../components/gutter"; import { SearchTextInput } from "../../../components/input"; import { Body2, + Body3, Subtitle2, Subtitle3, Subtitle4, @@ -34,7 +35,7 @@ import { useNavigate } from "react-router"; import { ChainImageFallback } from "../../../components/image"; import { Checkbox } from "../../../components/checkbox"; import { KeyRingCosmosService } from "@keplr-wallet/background"; -import { IChainInfoImpl, WalletStatus } from "@keplr-wallet/stores"; +import { WalletStatus } from "@keplr-wallet/stores"; import { ChainIdHelper } from "@keplr-wallet/cosmos"; import { TextButton } from "../../../components/button-text"; import { FormattedMessage, useIntl } from "react-intl"; @@ -42,6 +43,7 @@ import { Tag } from "../../../components/tag"; import SimpleBar from "simplebar-react"; import { useTheme } from "styled-components"; import { dispatchGlobalEventExceptSelf } from "../../../utils/global-events"; +import { VerticalCollapseTransition } from "../../../components/transition/vertical-collapse"; /** * EnableChainsScene은 finalize-key scene에서 선택한 chains를 활성화하는 scene이다. @@ -77,8 +79,14 @@ export const EnableChainsScene: FunctionComponent<{ skipWelcome, initialSearchValue, }) => { - const { chainStore, accountStore, queriesStore, priceStore, keyRingStore } = - useStore(); + const { + chainStore, + accountStore, + queriesStore, + priceStore, + keyRingStore, + starknetQueriesStore, + } = useStore(); const navigate = useNavigate(); const intl = useIntl(); @@ -143,46 +151,62 @@ export const EnableChainsScene: FunctionComponent<{ }[] = []; const promises: Promise[] = []; - for (const chainInfo of chainStore.chainInfos) { - if (keyRingStore.needKeyCoinTypeFinalize(vaultId, chainInfo)) { - promises.push( - (async () => { - const res = - await keyRingStore.computeNotFinalizedKeyAddresses( - vaultId, - chainInfo.chainId - ); + for (const modularChainInfo of chainStore.modularChainInfos) { + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain( + modularChainInfo.cosmos.chainId + ); + if (keyRingStore.needKeyCoinTypeFinalize(vaultId, chainInfo)) { + promises.push( + (async () => { + const res = + await keyRingStore.computeNotFinalizedKeyAddresses( + vaultId, + chainInfo.chainId + ); - candidateAddresses.push({ - chainId: chainInfo.chainId, - bech32Addresses: res.map((res) => { - return { - coinType: res.coinType, - address: res.bech32Address, - }; - }), - }); - })() + candidateAddresses.push({ + chainId: chainInfo.chainId, + bech32Addresses: res.map((res) => { + return { + coinType: res.coinType, + address: res.bech32Address, + }; + }), + }); + })() + ); + } else { + const account = accountStore.getAccount(chainInfo.chainId); + promises.push( + (async () => { + if (account.walletStatus !== WalletStatus.Loaded) { + await account.init(); + } + + if (account.bech32Address) { + candidateAddresses.push({ + chainId: chainInfo.chainId, + bech32Addresses: [ + { + coinType: chainInfo.bip44.coinType, + address: account.bech32Address, + }, + ], + }); + } + })() + ); + } + } else if ("starknet" in modularChainInfo) { + const account = accountStore.getAccount( + modularChainInfo.starknet.chainId ); - } else { - const account = accountStore.getAccount(chainInfo.chainId); promises.push( (async () => { if (account.walletStatus !== WalletStatus.Loaded) { await account.init(); } - - if (account.bech32Address) { - candidateAddresses.push({ - chainId: chainInfo.chainId, - bech32Addresses: [ - { - coinType: chainInfo.bip44.coinType, - address: account.bech32Address, - }, - ], - }); - } })() ); } @@ -340,6 +364,29 @@ export const EnableChainsScene: FunctionComponent<{ } } + // 스타크넷 관련 체인들은 `candidateAddresses`에 추가되지 않으므로 여기서 enable 할지 판단한다. + for (const modularChainInfo of chainStore.modularChainInfosInListUI) { + if ("starknet" in modularChainInfo) { + const account = accountStore.getAccount(modularChainInfo.chainId); + const mainCurrency = modularChainInfo.starknet.currencies[0]; + + const queryBalance = starknetQueriesStore + .get(modularChainInfo.chainId) + .queryStarknetERC20Balance.getBalance( + modularChainInfo.chainId, + chainStore, + account.starknetHexAddress, + mainCurrency.coinMinimalDenom + ); + + if (queryBalance && queryBalance.balance.toDec().gt(new Dec(0))) { + enabledChainIdentifiers.push( + ChainIdHelper.parse(modularChainInfo.chainId).identifier + ); + } + } + } + for (const candidateAddress of candidateAddresses) { const queries = queriesStore.get(candidateAddress.chainId); const chainInfo = chainStore.getChain(candidateAddress.chainId); @@ -445,170 +492,260 @@ export const EnableChainsScene: FunctionComponent<{ // 그리고 이를 토대로 balance에 따른 sort를 진행한다. // queries store의 구조 문제로 useMemo 안에서 balance에 따른 sort를 진행하긴 힘들다. // 그래서 이를 위한 변수로 따로 둔다. - // 실제로는 chainInfos를 사용하면 된다. - const preSortChainInfos = useMemo(() => { - let chainInfos = chainStore.chainInfosInListUI.slice(); + // 실제로는 modularChainInfos를 사용하면 된다. + const preSortModularChainInfos = useMemo(() => { + let modularChainInfos = chainStore.modularChainInfosInListUI.slice(); if (keyType === "ledger") { - chainInfos = chainInfos.filter((chainInfo) => { - const isEthermintLike = - chainInfo.bip44.coinType === 60 || - !!chainInfo.features?.includes("eth-address-gen") || - !!chainInfo.features?.includes("eth-key-sign"); - - // Ledger일 경우 ethereum app을 바로 처리할 수 없다. - // 이 경우 빼줘야한다. - if (isEthermintLike && !fallbackEthereumLedgerApp) { - return false; - } - - // fallbackEthereumLedgerApp가 true이면 ethereum app이 필요없는 체인은 이전에 다 처리된 것이다. - // 이게 true이면 ethereum app이 필요하고 가능한 체인만 남기면 된다. - if (fallbackEthereumLedgerApp) { - if (!isEthermintLike) { + modularChainInfos = modularChainInfos.filter((modularChainInfo) => { + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain( + modularChainInfo.cosmos.chainId + ); + const isEthermintLike = + chainInfo.bip44.coinType === 60 || + !!chainInfo.features?.includes("eth-address-gen") || + !!chainInfo.features?.includes("eth-key-sign"); + + // Ledger일 경우 ethereum app을 바로 처리할 수 없다. + // 이 경우 빼줘야한다. + if (isEthermintLike && !fallbackEthereumLedgerApp) { return false; } - try { - // 처리가능한 체인만 true를 반환한다. - KeyRingCosmosService.throwErrorIfEthermintWithLedgerButNotSupported( - chainInfo.chainId - ); - return true; - } catch { - return false; + // fallbackEthereumLedgerApp가 true이면 ethereum app이 필요없는 체인은 이전에 다 처리된 것이다. + // 이게 true이면 ethereum app이 필요하고 가능한 체인만 남기면 된다. + if (fallbackEthereumLedgerApp) { + if (!isEthermintLike) { + return false; + } + + try { + // 처리가능한 체인만 true를 반환한다. + KeyRingCosmosService.throwErrorIfEthermintWithLedgerButNotSupported( + chainInfo.chainId + ); + return true; + } catch { + return false; + } } + } else if ("starknet" in modularChainInfo) { + // TODO: Starknet ledger app 지원이 필요하면 여기에 로직을 추가한다. + return false; } return true; }); } - const trimSearch = search.trim(); + if (keyType === "keystone") { + modularChainInfos = modularChainInfos.filter((modularChainInfo) => { + // keystone은 스타크넷을 지원하지 않는다. + if ("starknet" in modularChainInfo) { + return false; + } + return true; + }); + } + const trimSearch = search.trim().toLowerCase(); if (!trimSearch) { - return chainInfos; + return modularChainInfos; } else { - return chainInfos.filter((chainInfo) => { - return ( - chainInfo.chainName - .toLowerCase() - .includes(trimSearch.toLowerCase()) || - (chainInfo.stakeCurrency || chainInfo.currencies[0]).coinDenom - .toLowerCase() - .includes(trimSearch.toLowerCase()) - ); - }); - } - }, [ - chainStore.chainInfosInListUI, - fallbackEthereumLedgerApp, - keyType, - search, - ]); - const chainInfos = preSortChainInfos.sort((a, b) => { - const aHasPriority = sortPriorityChainIdentifierMap.has( - a.chainIdentifier - ); - const bHasPriority = sortPriorityChainIdentifierMap.has( - b.chainIdentifier - ); + return modularChainInfos.filter((modularChainInfo) => { + if (modularChainInfo.chainName.toLowerCase().includes(trimSearch)) { + return true; + } - if (aHasPriority && !bHasPriority) { - return -1; + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain( + modularChainInfo.cosmos.chainId + ); + return ( + chainInfo.stakeCurrency || chainInfo.currencies[0] + ).coinDenom.includes(trimSearch); + } else if ("starknet" in modularChainInfo) { + return modularChainInfo.starknet.currencies[0].coinDenom.includes( + trimSearch + ); + } + }); } + }, [chainStore, fallbackEthereumLedgerApp, keyType, search]); + + const modularChainInfos = preSortModularChainInfos.sort( + (aModularChainInfo, bModularChainInfo) => { + const aChainIdentifier = ChainIdHelper.parse( + aModularChainInfo.chainId + ).identifier; + const bChainIdentifier = ChainIdHelper.parse( + bModularChainInfo.chainId + ).identifier; + const aHasPriority = + sortPriorityChainIdentifierMap.has(aChainIdentifier); + const bHasPriority = + sortPriorityChainIdentifierMap.has(bChainIdentifier); + + if (aHasPriority && !bHasPriority) { + return -1; + } - if (!aHasPriority && bHasPriority) { - return 1; - } + if (!aHasPriority && bHasPriority) { + return 1; + } - const aBalance = (() => { - const addresses = candidateAddressesMap.get(a.chainIdentifier); - const chainInfo = chainStore.getChain(a.chainId); - const queries = queriesStore.get(a.chainId); + const aBalance = (() => { + if ("cosmos" in aModularChainInfo) { + const addresses = candidateAddressesMap.get(aChainIdentifier); + const chainInfo = chainStore.getChain(aModularChainInfo.chainId); + const queries = queriesStore.get(aModularChainInfo.chainId); + + const mainCurrency = + chainInfo.stakeCurrency || chainInfo.currencies[0]; + const account = accountStore.getAccount(chainInfo.chainId); + + if (addresses && addresses.length > 0) { + const queryBalance = chainStore.isEvmOnlyChain(chainInfo.chainId) + ? queries.queryBalances.getQueryEthereumHexAddress( + account.ethereumHexAddress + ) + : queries.queryBalances.getQueryBech32Address( + addresses[0].address + ); + const balance = queryBalance.getBalance(mainCurrency)?.balance; - const mainCurrency = chainInfo.stakeCurrency || chainInfo.currencies[0]; - const account = accountStore.getAccount(chainInfo.chainId); + if (balance) { + return balance; + } + } - if (addresses && addresses.length > 0) { - const queryBalance = chainStore.isEvmOnlyChain(chainInfo.chainId) - ? queries.queryBalances.getQueryEthereumHexAddress( - account.ethereumHexAddress - ) - : queries.queryBalances.getQueryBech32Address(addresses[0].address); - const balance = queryBalance.getBalance(mainCurrency)?.balance; + return new CoinPretty(mainCurrency, "0"); + } else if ("starknet" in aModularChainInfo) { + const account = accountStore.getAccount(aModularChainInfo.chainId); + const mainCurrency = aModularChainInfo.starknet.currencies[0]; + + const queryBalance = starknetQueriesStore + .get(aModularChainInfo.chainId) + .queryStarknetERC20Balance.getBalance( + aModularChainInfo.chainId, + chainStore, + account.starknetHexAddress, + mainCurrency.coinMinimalDenom + ); - if (balance) { - return balance; + if (queryBalance) { + return queryBalance.balance; + } } - } + })(); + const bBalance = (() => { + if ("cosmos" in bModularChainInfo) { + const addresses = candidateAddressesMap.get(bChainIdentifier); + const chainInfo = chainStore.getChain(bModularChainInfo.chainId); + const queries = queriesStore.get(bModularChainInfo.chainId); + + const mainCurrency = + chainInfo.stakeCurrency || chainInfo.currencies[0]; + const account = accountStore.getAccount(chainInfo.chainId); + + if (addresses && addresses.length > 0) { + const queryBalance = chainStore.isEvmOnlyChain(chainInfo.chainId) + ? queries.queryBalances.getQueryEthereumHexAddress( + account.ethereumHexAddress + ) + : queries.queryBalances.getQueryBech32Address( + addresses[0].address + ); + const balance = queryBalance.getBalance(mainCurrency)?.balance; + + if (balance) { + return balance; + } + } - return new CoinPretty(mainCurrency, "0"); - })(); - const bBalance = (() => { - const addresses = candidateAddressesMap.get(b.chainIdentifier); - const chainInfo = chainStore.getChain(b.chainId); - const queries = queriesStore.get(b.chainId); - - const mainCurrency = chainInfo.stakeCurrency || chainInfo.currencies[0]; - const account = accountStore.getAccount(chainInfo.chainId); - - if (addresses && addresses.length > 0) { - const queryBalance = chainStore.isEvmOnlyChain(chainInfo.chainId) - ? queries.queryBalances.getQueryEthereumHexAddress( - account.ethereumHexAddress - ) - : queries.queryBalances.getQueryBech32Address(addresses[0].address); - const balance = queryBalance.getBalance(mainCurrency)?.balance; - - if (balance) { - return balance; + return new CoinPretty(mainCurrency, "0"); + } else if ("starknet" in bModularChainInfo) { + const account = accountStore.getAccount(bModularChainInfo.chainId); + const mainCurrency = bModularChainInfo.starknet.currencies[0]; + + const balance = starknetQueriesStore + .get(bModularChainInfo.chainId) + .queryStarknetERC20Balance.getBalance( + bModularChainInfo.chainId, + chainStore, + account.starknetHexAddress, + mainCurrency.coinMinimalDenom + )?.balance; + + if (balance) { + return balance; + } } - } + })(); - return new CoinPretty(mainCurrency, "0"); - })(); + const aPrice = aBalance + ? priceStore.calculatePrice(aBalance)?.toDec() ?? new Dec(0) + : new Dec(0); + const bPrice = bBalance + ? priceStore.calculatePrice(bBalance)?.toDec() ?? new Dec(0) + : new Dec(0); - const aPrice = priceStore.calculatePrice(aBalance)?.toDec() ?? new Dec(0); - const bPrice = priceStore.calculatePrice(bBalance)?.toDec() ?? new Dec(0); + if (!aPrice.equals(bPrice)) { + return aPrice.gt(bPrice) ? -1 : 1; + } - if (!aPrice.equals(bPrice)) { - return aPrice.gt(bPrice) ? -1 : 1; + // balance의 fiat 기준으로 sort. + // 같으면 이름 기준으로 sort. + return aModularChainInfo.chainName.localeCompare( + bModularChainInfo.chainName + ); } - - // balance의 fiat 기준으로 sort. - // 같으면 이름 기준으로 sort. - return a.chainName.localeCompare(b.chainName); - }); + ); const numSelected = useMemo(() => { - const chainInfoMap = new Map(); - for (const chanInfo of chainStore.chainInfos) { - chainInfoMap.set(chanInfo.chainIdentifier, chanInfo); + const modularChainInfoMap = new Map(); + for (const modularChainInfo of chainStore.modularChainInfos) { + modularChainInfoMap.set( + ChainIdHelper.parse(modularChainInfo.chainId).identifier, + modularChainInfo + ); } let numSelected = 0; for (const enabledChainIdentifier of enabledChainIdentifiers) { - const enabledChainInfo = chainInfoMap.get(enabledChainIdentifier); - if (enabledChainInfo) { + const enabledModularChainInfo = modularChainInfoMap.get( + enabledChainIdentifier + ); + if (enabledModularChainInfo) { const isEthermintLike = - enabledChainInfo.bip44.coinType === 60 || - !!enabledChainInfo.features?.includes("eth-address-gen") || - !!enabledChainInfo.features?.includes("eth-key-sign"); - - if ( - (fallbackEthereumLedgerApp && isEthermintLike) || - (!fallbackEthereumLedgerApp && !isEthermintLike) - ) { + "cosmos" in enabledModularChainInfo && + (enabledModularChainInfo.cosmos.bip44.coinType === 60 || + !!enabledModularChainInfo.cosmos.features?.includes( + "eth-address-gen" + ) || + !!enabledModularChainInfo.cosmos.features?.includes( + "eth-key-sign" + )); + + if (keyType === "ledger") { + if ( + (fallbackEthereumLedgerApp && isEthermintLike) || + (!fallbackEthereumLedgerApp && !isEthermintLike) + ) { + numSelected++; + } + } else { numSelected++; } } } return numSelected; }, [ - chainStore.chainInfos, + chainStore.modularChainInfos, enabledChainIdentifiers, fallbackEthereumLedgerApp, + keyType, ]); const replaceToWelcomePage = () => { @@ -623,11 +760,13 @@ export const EnableChainsScene: FunctionComponent<{ const enabledChainIdentifiersInPage = useMemo(() => { return enabledChainIdentifiers.filter((chainIdentifier) => - chainInfos.some( - (chainInfo) => chainIdentifier === chainInfo.chainIdentifier + modularChainInfos.some( + (modularChainInfo) => + chainIdentifier === + ChainIdHelper.parse(modularChainInfo.chainId).identifier ) ); - }, [enabledChainIdentifiers, chainInfos]); + }, [enabledChainIdentifiers, modularChainInfos]); const [preSelectedChainIdentifiers, setPreSelectedChainIdentifiers] = useState([]); @@ -672,34 +811,57 @@ export const EnableChainsScene: FunctionComponent<{ }} > - {chainInfos.map((chainInfo) => { - const account = accountStore.getAccount(chainInfo.chainId); - const queries = queriesStore.get(chainInfo.chainId); - const mainCurrency = - chainInfo.stakeCurrency || chainInfo.currencies[0]; - + {modularChainInfos.map((modularChainInfo) => { + const account = accountStore.getAccount(modularChainInfo.chainId); const balance = (() => { - const queryBalance = chainStore.isEvmOnlyChain( - chainInfo.chainId - ) - ? queries.queryBalances.getQueryEthereumHexAddress( - account.ethereumHexAddress - ) - : queries.queryBalances.getQueryBech32Address( - account.bech32Address + if ("cosmos" in modularChainInfo) { + const chainInfo = chainStore.getChain( + modularChainInfo.cosmos.chainId + ); + const queries = queriesStore.get(modularChainInfo.chainId); + const mainCurrency = + chainInfo.stakeCurrency || chainInfo.currencies[0]; + + const queryBalance = chainStore.isEvmOnlyChain( + chainInfo.chainId + ) + ? queries.queryBalances.getQueryEthereumHexAddress( + account.ethereumHexAddress + ) + : queries.queryBalances.getQueryBech32Address( + account.bech32Address + ); + const balance = queryBalance.getBalance(mainCurrency); + + if (balance) { + return balance.balance; + } + + return new CoinPretty(mainCurrency, "0"); + } else if ("starknet" in modularChainInfo) { + const mainCurrency = modularChainInfo.starknet.currencies[0]; + const queryBalance = starknetQueriesStore + .get(modularChainInfo.chainId) + .queryStarknetERC20Balance.getBalance( + modularChainInfo.chainId, + chainStore, + account.starknetHexAddress, + mainCurrency.coinMinimalDenom ); - const balance = queryBalance.getBalance(mainCurrency); - if (balance) { - return balance.balance; - } + if (queryBalance) { + return queryBalance.balance; + } - return new CoinPretty(mainCurrency, "0"); + return new CoinPretty(mainCurrency, "0"); + } })(); + const chainIdentifier = ChainIdHelper.parse( + modularChainInfo.chainId + ).identifier; const enabled = - enabledChainIdentifierMap.get(chainInfo.chainIdentifier) || - false; + enabledChainIdentifierMap.get(chainIdentifier) || false; // At least, one chain should be enabled. const blockInteraction = @@ -707,26 +869,23 @@ export const EnableChainsScene: FunctionComponent<{ return ( { - if ( - enabledChainIdentifierMap.get(chainInfo.chainIdentifier) - ) { + if (enabledChainIdentifierMap.get(chainIdentifier)) { setEnabledChainIdentifiers( enabledChainIdentifiers.filter( - (chainIdentifier) => - chainIdentifier !== chainInfo.chainIdentifier + (ci) => ci !== chainIdentifier ) ); } else { setEnabledChainIdentifiers([ ...enabledChainIdentifiers, - chainInfo.chainIdentifier, + chainIdentifier, ]); } }} @@ -781,7 +940,6 @@ export const EnableChainsScene: FunctionComponent<{ })} - @@ -793,14 +951,16 @@ export const EnableChainsScene: FunctionComponent<{ e.preventDefault(); if ( - chainInfos.length === enabledChainIdentifiersInPage.length + modularChainInfos.length === + enabledChainIdentifiersInPage.length ) { if (preSelectedChainIdentifiers.length > 0) { setEnabledChainIdentifiers(preSelectedChainIdentifiers); } else { - if (chainInfos.length > 0) { + if (modularChainInfos.length > 0) { setEnabledChainIdentifiers([ - chainInfos[0].chainIdentifier, + ChainIdHelper.parse(modularChainInfos[0].chainId) + .identifier, ]); } } @@ -808,15 +968,12 @@ export const EnableChainsScene: FunctionComponent<{ setPreSelectedChainIdentifiers([...enabledChainIdentifiers]); const newEnabledChainIdentifiers: string[] = enabledChainIdentifiers.slice(); - for (const chainInfo of chainInfos) { - if ( - !newEnabledChainIdentifiers.includes( - chainInfo.chainIdentifier - ) - ) { - newEnabledChainIdentifiers.push( - chainInfo.chainIdentifier - ); + for (const modularChainInfo of modularChainInfos) { + const chainIdentifier = ChainIdHelper.parse( + modularChainInfo.chainId + ).identifier; + if (!newEnabledChainIdentifiers.includes(chainIdentifier)) { + newEnabledChainIdentifiers.push(chainIdentifier); } } setEnabledChainIdentifiers(newEnabledChainIdentifiers); @@ -839,7 +996,8 @@ export const EnableChainsScene: FunctionComponent<{ {}} /> @@ -848,6 +1006,70 @@ export const EnableChainsScene: FunctionComponent<{ + { + for (const chainIdentifier of enabledChainIdentifiersInPage) { + const modularChainInfo = + chainStore.getModularChain(chainIdentifier); + if ("starknet" in modularChainInfo) { + return false; + } + } + return true; + })()} + > + + + + + + + + + + + + + + + + + +