From 4f805ca96a2d77bf68ff123e80678e4df6639307 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Wed, 19 Jun 2024 19:40:28 -0400 Subject: [PATCH 01/33] feat: display immersive bridge assets --- .../src/queries/complex/assets/bridge.ts | 10 +- .../src/queries/complex/assets/index.ts | 25 +- .../src/queries/complex/assets/market.ts | 8 +- .../server/src/queries/complex/assets/user.ts | 10 +- .../src/queries/complex/earn/strategies.ts | 12 +- .../src/queries/complex/pools/transmuter.ts | 8 +- .../server/src/queries/data-services/earn.ts | 8 +- packages/trpc/src/assets.ts | 92 +++++- packages/types/src/asset-types.ts | 9 + packages/utils/src/asset-utils.ts | 12 +- packages/web/components/bridge/flow.ts | 8 - .../bridge/{ => immersive}/amount-screen.tsx | 6 +- .../bridge/immersive/assets-screen.tsx | 218 ++++++++++++++ .../{ => immersive}/bridge-network-select.tsx | 44 ++- .../bridge/immersive/immersive-bridge.tsx | 277 ++++++++++++++++++ .../web/components/bridge/immersive/index.ts | 2 +- .../{ => immersive}/more-bridge-options.tsx | 0 .../components/bridge/immersive/provider.tsx | 143 --------- packages/web/components/bridge/legacy.tsx | 23 +- .../web/components/complex/assets-page-v1.tsx | 8 +- .../web/components/complex/portfolio-page.tsx | 10 +- .../web/components/stepper/progress-bar.tsx | 10 +- .../components/your-balance/your-balance.tsx | 8 +- packages/web/config/generate-lists.ts | 21 ++ packages/web/hooks/bridge.tsx | 15 +- packages/web/hooks/use-swap.tsx | 2 +- .../web/modals/fiat-on-ramp-selection.tsx | 2 +- 27 files changed, 736 insertions(+), 255 deletions(-) delete mode 100644 packages/web/components/bridge/flow.ts rename packages/web/components/bridge/{ => immersive}/amount-screen.tsx (98%) create mode 100644 packages/web/components/bridge/immersive/assets-screen.tsx rename packages/web/components/bridge/{ => immersive}/bridge-network-select.tsx (78%) create mode 100644 packages/web/components/bridge/immersive/immersive-bridge.tsx rename packages/web/components/bridge/{ => immersive}/more-bridge-options.tsx (100%) delete mode 100644 packages/web/components/bridge/immersive/provider.tsx diff --git a/packages/server/src/queries/complex/assets/bridge.ts b/packages/server/src/queries/complex/assets/bridge.ts index b0d2d41a8d..47ee891d46 100644 --- a/packages/server/src/queries/complex/assets/bridge.ts +++ b/packages/server/src/queries/complex/assets/bridge.ts @@ -1,6 +1,8 @@ -import { Asset as AssetListAsset, AssetList } from "@osmosis-labs/types"; - -import { Asset } from "."; +import { + Asset as AssetListAsset, + AssetList, + MinimalAsset, +} from "@osmosis-labs/types"; /** A bridgeable asset. */ export type BridgeAsset = { @@ -11,7 +13,7 @@ export type BridgeAsset = { /** Appends bridge info to a given asset. If asset is not found in asset list, empty bridge info will be returned. * @throws if a given asset is not found in asset list. */ -export function getBridgeAsset( +export function getBridgeAsset( assetLists: AssetList[], asset: TAsset ): TAsset & BridgeAsset { diff --git a/packages/server/src/queries/complex/assets/index.ts b/packages/server/src/queries/complex/assets/index.ts index 2ec0e2d8ad..dc619e92ed 100644 --- a/packages/server/src/queries/complex/assets/index.ts +++ b/packages/server/src/queries/complex/assets/index.ts @@ -1,5 +1,9 @@ import { CoinPretty } from "@keplr-wallet/unit"; -import { Asset as AssetListAsset, AssetList } from "@osmosis-labs/types"; +import { + Asset as AssetListAsset, + AssetList, + MinimalAsset, +} from "@osmosis-labs/types"; import { makeMinimalAsset } from "@osmosis-labs/utils"; import { z } from "zod"; @@ -7,19 +11,6 @@ import { captureErrorAndReturn } from "../../../utils/error"; import { search, SearchSchema } from "../../../utils/search"; import { isAssetInCategories } from "./categories"; -/** An asset with minimal data that conforms to `Currency` type. */ -export type Asset = { - coinDenom: string; - coinName: string; - coinMinimalDenom: string; - coinDecimals: number; - coinGeckoId: string | undefined; - coinImageUrl?: string; - isVerified: boolean; - isUnstable: boolean; - sourceDenom: string; -}; - export const AssetFilterSchema = z.object({ search: SearchSchema.optional(), onlyVerified: z.boolean().default(false).optional(), @@ -37,7 +28,7 @@ export function getAsset({ }: { assetLists: AssetList[]; anyDenom: string; -}): Asset { +}): MinimalAsset { const assets = getAssets({ assetLists, findMinDenomOrSymbol: anyDenom, @@ -62,7 +53,7 @@ export function getAssets({ assetLists: AssetList[]; /** Explicitly match the base or symbol denom. */ findMinDenomOrSymbol?: string; -} & AssetFilter): Asset[] { +} & AssetFilter): MinimalAsset[] { return filterAssetList(assetLists, params); } @@ -99,7 +90,7 @@ function filterAssetList( params: { findMinDenomOrSymbol?: string; } & AssetFilter -): Asset[] { +): MinimalAsset[] { // Create new array with just assets const coinMinimalDenomSet = new Set(); diff --git a/packages/server/src/queries/complex/assets/market.ts b/packages/server/src/queries/complex/assets/market.ts index 336b6a1b7b..cf236464b3 100644 --- a/packages/server/src/queries/complex/assets/market.ts +++ b/packages/server/src/queries/complex/assets/market.ts @@ -1,5 +1,5 @@ import { Dec, PricePretty, RatePretty } from "@keplr-wallet/unit"; -import { AssetList, Chain } from "@osmosis-labs/types"; +import { AssetList, Chain, MinimalAsset } from "@osmosis-labs/types"; import cachified, { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; @@ -12,7 +12,7 @@ import { queryTokenMarketCaps, TokenData, } from "../../data-services"; -import { Asset, AssetFilter, getAssets } from "."; +import { AssetFilter, getAssets } from "."; import { DEFAULT_VS_CURRENCY } from "./config"; export type AssetMarketInfo = Partial<{ @@ -26,7 +26,7 @@ export type AssetMarketInfo = Partial<{ const marketInfoCache = new LRUCache(DEFAULT_LRU_OPTIONS); /** Cached function that returns an asset with market info included. */ -export async function getMarketAsset({ +export async function getMarketAsset({ asset, }: { asset: TAsset; @@ -63,7 +63,7 @@ export async function getMarketAsset({ /** Maps and adds general supplementary market data such as current price and market cap to the given type. * If no assets provided, they will be fetched and passed the given search params. */ -export async function mapGetMarketAssets({ +export async function mapGetMarketAssets({ assets, ...params }: { diff --git a/packages/server/src/queries/complex/assets/user.ts b/packages/server/src/queries/complex/assets/user.ts index f4a6775505..85f7cade1b 100644 --- a/packages/server/src/queries/complex/assets/user.ts +++ b/packages/server/src/queries/complex/assets/user.ts @@ -1,5 +1,5 @@ import { CoinPretty, PricePretty } from "@keplr-wallet/unit"; -import { AssetList, Chain } from "@osmosis-labs/types"; +import { AssetList, Chain, MinimalAsset } from "@osmosis-labs/types"; import { aggregateCoinsByDenom, isNil, @@ -16,7 +16,7 @@ import { getUserTotalDelegatedCoin, getUserTotalUndelegations, } from "../staking/user"; -import { Asset, AssetFilter, calcSumCoinsValue, getAsset, getAssets } from "."; +import { AssetFilter, calcSumCoinsValue, getAsset, getAssets } from "."; import { DEFAULT_VS_CURRENCY } from "./config"; import { calcAssetValue } from "./price"; @@ -27,7 +27,7 @@ export type MaybeUserAssetCoin = Partial<{ }>; /** Given an asset, appends the user's balance if applicable. */ -export async function getAssetWithUserBalance({ +export async function getAssetWithUserBalance({ assetLists, chainList, asset, @@ -53,7 +53,9 @@ export async function getAssetWithUserBalance({ /** Maps user coin data given a list of assets of a given type and a potential user Osmosis address. * If no assets provided, they will be fetched and passed the given search params. * If no search param is provided and `sortFiatValueDirection` is defined, it will sort by user fiat value. */ -export async function mapGetAssetsWithUserBalances({ +export async function mapGetAssetsWithUserBalances< + TAsset extends MinimalAsset +>({ poolId, ...params }: { diff --git a/packages/server/src/queries/complex/earn/strategies.ts b/packages/server/src/queries/complex/earn/strategies.ts index 016f1e7b32..2d25afb447 100644 --- a/packages/server/src/queries/complex/earn/strategies.ts +++ b/packages/server/src/queries/complex/earn/strategies.ts @@ -1,5 +1,5 @@ import { Dec, PricePretty, RatePretty } from "@keplr-wallet/unit"; -import { AssetList } from "@osmosis-labs/types"; +import { AssetList, MinimalAsset } from "@osmosis-labs/types"; import cachified, { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; @@ -19,7 +19,7 @@ import { queryOsmosisCMS } from "../../../queries/github"; import { DEFAULT_LRU_OPTIONS } from "../../../utils/cache"; import { dayjs } from "../../../utils/dayjs"; import { captureIfError } from "../../../utils/error"; -import { type Asset, getAsset } from "../assets"; +import { getAsset } from "../assets"; import { DEFAULT_VS_CURRENCY } from "../assets/config"; import { convertToPricePretty } from "../price"; @@ -157,11 +157,13 @@ export async function getStrategies({ ...rawStrategy, depositAssets: depositAssets.filter( (deposit) => !!deposit - ) as Asset[], + ) as MinimalAsset[], positionAssets: positionAssets.filter( (position) => !!position - ) as Asset[], - rewardAssets: rewardAssets.filter((reward) => !!reward) as Asset[], + ) as MinimalAsset[], + rewardAssets: rewardAssets.filter( + (reward) => !!reward + ) as MinimalAsset[], hasLockingDuration: dayjs.duration(lockDuration).asMilliseconds() > 0, }); diff --git a/packages/server/src/queries/complex/pools/transmuter.ts b/packages/server/src/queries/complex/pools/transmuter.ts index c72e99751b..c71af94789 100644 --- a/packages/server/src/queries/complex/pools/transmuter.ts +++ b/packages/server/src/queries/complex/pools/transmuter.ts @@ -1,11 +1,11 @@ import { CoinPretty, Dec, RatePretty } from "@keplr-wallet/unit"; -import { AssetList, Chain } from "@osmosis-labs/types"; +import { AssetList, Chain, MinimalAsset } from "@osmosis-labs/types"; import cachified, { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; import { captureIfError, DEFAULT_LRU_OPTIONS } from "../../../utils"; import { queryTransmuterTotalPoolLiquidity } from "../../contracts"; -import { Asset, getAsset } from "../assets"; +import { getAsset } from "../assets"; const transmuterTotalPoolLiquidityCache = new LRUCache( DEFAULT_LRU_OPTIONS @@ -22,13 +22,13 @@ export async function getCachedTransmuterTotalPoolLiquidity( key: `transmuter-total-pool-liquidity-${contractAddress}`, getFreshValue: async (): Promise< Array<{ - asset: Asset; + asset: MinimalAsset; coin: CoinPretty; percentage: RatePretty; }> > => { const poolLiquidityAssets: Array<{ - asset: Asset; + asset: MinimalAsset; coin: CoinPretty; percentage: RatePretty; }> = []; diff --git a/packages/server/src/queries/data-services/earn.ts b/packages/server/src/queries/data-services/earn.ts index 15e35e9378..b962d37eed 100644 --- a/packages/server/src/queries/data-services/earn.ts +++ b/packages/server/src/queries/data-services/earn.ts @@ -1,8 +1,8 @@ import { PricePretty, RatePretty } from "@keplr-wallet/unit"; +import { MinimalAsset } from "@osmosis-labs/types"; import { apiClient } from "@osmosis-labs/utils"; import { NUMIA_BASE_URL } from "../../env"; -import { Asset } from "../../queries/complex/assets"; export const EarnStrategyCategories = [ "Staking", @@ -197,15 +197,15 @@ export interface StrategyCMSData { /** * Array describing assets deposited for participation in the strategy. */ - depositAssets: Asset[]; + depositAssets: MinimalAsset[]; /** * Array describing assets representing a position in the strategy. */ - positionAssets: Asset[]; + positionAssets: MinimalAsset[]; /** * Array describing rewarded assets for participating in the strategy. */ - rewardAssets: Asset[]; + rewardAssets: MinimalAsset[]; /** * Array of tags associated with the strategy. * The currently accepted tags are: diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index d1933fcd7a..81e02024ab 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -26,7 +26,7 @@ import { TimeDuration, TimeFrame, } from "@osmosis-labs/server"; -import { compareCommon, sort } from "@osmosis-labs/utils"; +import { compareCommon, isNil, sort } from "@osmosis-labs/utils"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; @@ -498,4 +498,94 @@ export const assetsRouter = createTRPCRouter({ upcomingAssets.slice(0, topN) ) ), + getImmersiveBridgeAssets: publicProcedure + .input( + GetInfiniteAssetsInputSchema.omit({ + categories: true, + onlyVerified: true, + }) + .merge(UserOsmoAddressSchema) + .merge( + z.object({ + variantsNotToBeExcluded: z.array(z.string()), + prioritizedDenoms: z.array(z.string()), + type: z.union([z.literal("deposit"), z.literal("withdraw")]), + }) + ) + ) + .query( + async ({ + input: { + search, + userOsmoAddress, + limit, + cursor, + includePreview, + variantsNotToBeExcluded, + prioritizedDenoms, + type, + }, + ctx, + }) => + maybeCachePaginatedItems({ + getFreshItems: async () => { + let assets = await mapGetAssetsWithUserBalances({ + ...ctx, + search, + // Only get balances for withdraw + userOsmoAddress: + type === "withdraw" ? userOsmoAddress : undefined, + sortFiatValueDirection: "desc", + includePreview, + }); + + if (type === "withdraw") { + assets = assets + // Filter out all assets without amount + .filter((asset) => !isNil(asset.amount)); + } + + if (type === "deposit") { + assets = assets + // Filter out all asset variants to encourage users to deposit and convert to the canonical asset + .filter((asset) => { + if ( + !isNil(asset.variantGroupKey) && + !variantsNotToBeExcluded.includes( + asset.variantGroupKey as (typeof variantsNotToBeExcluded)[number] + ) + ) { + return asset.variantGroupKey === asset.coinDenom; + } + + return true; + }); + } + + return assets.sort((a, b) => { + const aIndex = prioritizedDenoms.indexOf( + a.coinDenom as (typeof prioritizedDenoms)[number] + ); + const bIndex = prioritizedDenoms.indexOf( + b.coinDenom as (typeof prioritizedDenoms)[number] + ); + + if (aIndex === -1 && bIndex === -1) return 0; // Both not prioritized + if (aIndex === -1) return 1; // a is not prioritized, b is + if (bIndex === -1) return -1; // b is not prioritized, a is + + return aIndex - bIndex; // Both are prioritized, sort by their index + }); + }, + cacheKey: JSON.stringify({ + search, + userOsmoAddress, + includePreview, + variantsToBeExcluded: variantsNotToBeExcluded, + prioritizedDenoms, + }), + cursor, + limit, + }) + ), }); diff --git a/packages/types/src/asset-types.ts b/packages/types/src/asset-types.ts index da1482663c..51c0836f9c 100644 --- a/packages/types/src/asset-types.ts +++ b/packages/types/src/asset-types.ts @@ -179,3 +179,12 @@ export interface Asset { /** Denom key of variant of asset this is grouped with. */ variantGroupKey?: string; } + +export type MinimalAsset = Currency & { + coinGeckoId: string | undefined; + coinName: string; + isUnstable: boolean; + isVerified: boolean; + sourceDenom: string; + variantGroupKey: string | undefined; +}; diff --git a/packages/utils/src/asset-utils.ts b/packages/utils/src/asset-utils.ts index 95ff418799..8b6ccaeac4 100644 --- a/packages/utils/src/asset-utils.ts +++ b/packages/utils/src/asset-utils.ts @@ -1,4 +1,4 @@ -import type { Asset, AssetList, Currency } from "@osmosis-labs/types"; +import type { Asset, AssetList, MinimalAsset } from "@osmosis-labs/types"; /** Find asset in asset list config given any of the available identifiers. */ export function getAssetFromAssetList({ @@ -50,13 +50,7 @@ export function getAssetFromAssetList({ /** Convert an asset list asset into an asset with minimal content and that * is compliant with the `Currency` type. */ -export function makeMinimalAsset(assetListAsset: Asset): Currency & { - coinGeckoId: string | undefined; - coinName: string; - isUnstable: boolean; - isVerified: boolean; - sourceDenom: string; -} { +export function makeMinimalAsset(assetListAsset: Asset): MinimalAsset { const { decimals, symbol, @@ -67,6 +61,7 @@ export function makeMinimalAsset(assetListAsset: Asset): Currency & { unstable, verified, sourceDenom, + variantGroupKey, } = assetListAsset; return { @@ -79,5 +74,6 @@ export function makeMinimalAsset(assetListAsset: Asset): Currency & { coinImageUrl: relative_image_url, isUnstable: unstable, isVerified: verified, + variantGroupKey, }; } diff --git a/packages/web/components/bridge/flow.ts b/packages/web/components/bridge/flow.ts deleted file mode 100644 index 336dd60122..0000000000 --- a/packages/web/components/bridge/flow.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Provider } from "react"; - -import { BridgeContext } from "~/hooks/bridge"; - -/** A bridge flow for UI for deposit/withdraw or fiat on ramping capable of handling the bridge context provider. */ -export interface BridgeFlowProvider { - Provider: Provider; -} diff --git a/packages/web/components/bridge/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx similarity index 98% rename from packages/web/components/bridge/amount-screen.tsx rename to packages/web/components/bridge/immersive/amount-screen.tsx index f397742cd7..6d71f0a3cb 100644 --- a/packages/web/components/bridge/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -8,8 +8,8 @@ import Image from "next/image"; import { FunctionComponent, ReactNode, useState } from "react"; import { Icon } from "~/components/assets"; -import { BridgeNetworkSelect } from "~/components/bridge/bridge-network-select"; -import { MoreBridgeOptions } from "~/components/bridge/more-bridge-options"; +import { BridgeNetworkSelect } from "~/components/bridge/immersive/bridge-network-select"; +import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; import { InputBox } from "~/components/input"; import { Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; @@ -103,7 +103,7 @@ export const AmountScreen = observer(({ type }: AmountScreenProps) => { }; return ( -
+
{type === "deposit" ? t("transfer.deposit") : t("transfer.withdraw")} diff --git a/packages/web/components/bridge/immersive/assets-screen.tsx b/packages/web/components/bridge/immersive/assets-screen.tsx new file mode 100644 index 0000000000..fe0922f4cf --- /dev/null +++ b/packages/web/components/bridge/immersive/assets-screen.tsx @@ -0,0 +1,218 @@ +import { MinimalAsset } from "@osmosis-labs/types"; +import classNames from "classnames"; +import debounce from "debounce"; +import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import React, { useMemo, useState } from "react"; + +import { Icon } from "~/components/assets"; +import { SearchBox } from "~/components/input"; +import { Intersection } from "~/components/intersection"; +import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { Tooltip } from "~/components/tooltip"; +import { + MainnetAssetSymbols, + MainnetVariantGroupKeys, + TestnetAssetSymbols, + TestnetVariantGroupKeys, +} from "~/config/generated/asset-lists"; +import { useTranslation } from "~/hooks"; +import { useShowPreviewAssets } from "~/hooks/use-show-preview-assets"; +import { ActivateUnverifiedTokenConfirmation } from "~/modals/activate-unverified-token-confirmation"; +import { useStore } from "~/stores"; +import { UnverifiedAssetsState } from "~/stores/user-settings/unverified-assets"; +import { formatPretty } from "~/utils/formatter"; +import { api } from "~/utils/trpc"; + +const variantsNotToBeExcluded = ["WBTC", "BTC"] satisfies ( + | MainnetVariantGroupKeys + | TestnetVariantGroupKeys +)[]; +const prioritizedDenoms = [ + "USDC", + "OSMO", + "ETH", + "SOL", + "USDT", + "WBTC", + "ATOM", + "TIA", +] satisfies (MainnetAssetSymbols | TestnetAssetSymbols)[]; + +interface AssetsScreenProps { + type: "deposit" | "withdraw"; + onSelectAsset: (asset: MinimalAsset) => void; +} + +export const AssetsScreen = observer( + ({ type, onSelectAsset }: AssetsScreenProps) => { + const { accountStore, userSettings } = useStore(); + const { showPreviewAssets } = useShowPreviewAssets(); + const { t } = useTranslation(); + + const wallet = accountStore.getWallet(accountStore.osmosisChainId); + + const [search, setSearch] = useState(""); + const [assetToActivate, setAssetToActivate] = useState( + null + ); + + const showUnverifiedAssetsSetting = + userSettings.getUserSettingById( + "unverified-assets" + ); + const shouldShowUnverifiedAssets = + showUnverifiedAssetsSetting?.state.showUnverifiedAssets; + + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + api.edge.assets.getImmersiveBridgeAssets.useInfiniteQuery( + { + type, + search: Boolean(search) + ? { + query: search, + } + : undefined, + userOsmoAddress: wallet?.address, + includePreview: showPreviewAssets, + variantsNotToBeExcluded, + prioritizedDenoms, + limit: 50, // items per page + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialCursor: 0, + + // avoid blocking + trpc: { + context: { + skipBatch: true, + }, + }, + } + ); + + const assets = useMemo( + () => data?.pages.flatMap((page) => page?.items) ?? [], + [data?.pages] + ); + const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; + + return ( +
+ { + if (!assetToActivate) return; + showUnverifiedAssetsSetting?.setState({ + showUnverifiedAssets: true, + }); + onSelectAsset(assetToActivate); + }} + onRequestClose={() => { + setAssetToActivate(null); + }} + /> + + { + setSearch(nextValue); + }, 300)} + className="my-4 flex-shrink-0" + placeholder="Search assets" + size="full" + /> + +
+ {isLoading ? ( + <> + {new Array(7).fill(undefined).map((_, i) => ( + + ))} + + ) : ( + <> + {assets.map((asset) => ( + + ))} + { + if (canLoadMore) { + fetchNextPage(); + } + }} + /> + {isFetchingNextPage && ( +
+ +
+ )} + + )} +
+
+ ); + } +); diff --git a/packages/web/components/bridge/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx similarity index 78% rename from packages/web/components/bridge/bridge-network-select.tsx rename to packages/web/components/bridge/immersive/bridge-network-select.tsx index 7eb2baa33c..650d38dc74 100644 --- a/packages/web/components/bridge/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -9,34 +9,29 @@ import { api } from "~/utils/trpc"; export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { const [query, setQuery] = useState(""); - const { - data: chainsPages, - hasNextPage, - isLoading, - isFetchingNextPage, - fetchNextPage, - } = api.edge.chains.getChains.useInfiniteQuery( - { - limit: 50, - search: query, - }, - { - enabled: modalProps.isOpen, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialCursor: 0, - keepPreviousData: true, + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + api.edge.chains.getChains.useInfiniteQuery( + { + limit: 50, + search: query, + }, + { + enabled: modalProps.isOpen, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialCursor: 0, + keepPreviousData: true, - trpc: { - context: { - skipBatch: true, + trpc: { + context: { + skipBatch: true, + }, }, - }, - } - ); + } + ); const chains = useMemo( - () => chainsPages?.pages.flatMap((page) => page?.items) ?? [], - [chainsPages] + () => data?.pages.flatMap((page) => page?.items) ?? [], + [data] ); const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; @@ -46,6 +41,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { className="!max-w-[30rem]" {...modalProps} > + {/* TODO: Add translation */} { setQuery(nextValue); diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx new file mode 100644 index 0000000000..e1a3eb0783 --- /dev/null +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -0,0 +1,277 @@ +import { Transition } from "@headlessui/react"; +import { BridgeAsset } from "@osmosis-labs/bridge"; +import { isNil } from "@osmosis-labs/utils"; +import { PropsWithChildren, useState } from "react"; +import { useLockBodyScroll } from "react-use"; + +import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { AssetsScreen } from "~/components/bridge/immersive/assets-screen"; +import { Screen, ScreenManager } from "~/components/screen-manager"; +import { StepProgress } from "~/components/stepper/progress-bar"; +import { Button } from "~/components/ui/button"; +import { EventName } from "~/config"; +import { BridgeFlowProvider } from "~/hooks/bridge"; +import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; +import { useDisclosure } from "~/hooks/use-disclosure"; +import { FiatRampKey } from "~/integrations"; +import { ModalCloseButton } from "~/modals"; +import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; +import { FiatRampsModal } from "~/modals/fiat-ramps"; +import { api } from "~/utils/trpc"; + +enum ImmersiveBridgeScreens { + Asset = "0", + Amount = "1", + Review = "2", +} + +export const ImmersiveBridgeFlow = ({ + Provider, + children, +}: PropsWithChildren) => { + const [isVisible, setIsVisible] = useState(false); + const [step, setStep] = useState( + ImmersiveBridgeScreens.Asset + ); + const [type, setType] = useState<"deposit" | "withdraw">("deposit"); + const { logEvent } = useAmplitudeAnalytics(); + + const apiUtils = api.useUtils(); + + const [osmosisAsset, setOsmosisAsset] = useState(); + + const [fiatRampParams, setFiatRampParams] = useState<{ + fiatRampKey: FiatRampKey; + assetKey: string; + } | null>(null); + + const { + isOpen: isFiatOnrampSelectionOpen, + onOpen: onOpenFiatOnrampSelection, + onClose: onCloseFiatOnrampSelection, + } = useDisclosure(); + + // const { isConnected, address } = useEvmWalletAccount(); + // const { onOpenWalletSelect } = useWalletSelect(); + // const { disconnect } = useDisconnectEvmWallet(); + + useLockBodyScroll(isVisible); + + const onClose = () => { + setIsVisible(false); + setOsmosisAsset(undefined); + setStep(ImmersiveBridgeScreens.Asset); + }; + + const onOpen = (direction: "deposit" | "withdraw") => { + setIsVisible(true); + setType(direction); + }; + + return ( + { + onOpen(direction); + console.log("startBridge", direction); + }, + bridgeAsset: async ({ + anyDenom, + direction, + }: { + anyDenom: string; + direction: "deposit" | "withdraw"; + }) => { + onOpen(direction); + + const fetchAssetWithRetry = async (retries = 3) => { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const asset = await apiUtils.edge.assets.getUserAsset.fetch({ + findMinDenomOrSymbol: anyDenom, + }); + + if (!asset) { + console.error("Asset not found", anyDenom); + return undefined; + } + + return asset; + } catch (error) { + console.error(`Attempt ${attempt} failed:`, error); + if (attempt === retries) { + console.error("All attempts to fetch asset failed"); + return undefined; + } + } + } + }; + + const asset = await fetchAssetWithRetry(); + + if (!asset) { + return; + } + + setOsmosisAsset({ + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + denom: asset.coinDenom, + sourceDenom: asset.sourceDenom, + }); + console.log("bridgeAsset", anyDenom, direction); + }, + fiatRamp: ({ + fiatRampKey, + assetKey, + }: { + fiatRampKey: FiatRampKey; + assetKey: string; + }) => { + setFiatRampParams({ fiatRampKey, assetKey }); + }, + fiatRampSelection: onOpenFiatOnrampSelection, + }} + > + {children} + { + return setStep(screen as ImmersiveBridgeScreens); + }} + > + + onClose()} /> + +
+ setStep(ImmersiveBridgeScreens.Asset) + : undefined, + }, + { + displayLabel: "Amount", + onClick: + step === ImmersiveBridgeScreens.Review + ? () => setStep(ImmersiveBridgeScreens.Amount) + : undefined, + }, + { + displayLabel: "Review", + }, + ]} + currentStep={Number(step)} + /> + +
+ + {({ setCurrentScreen }) => ( + { + setCurrentScreen(ImmersiveBridgeScreens.Amount); + setOsmosisAsset({ + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + denom: asset.coinDenom, + sourceDenom: asset.sourceDenom, + }); + }} + /> + )} + + + {({ setCurrentScreen, goBack }) => ( +
+
+ + + +
+ + +
+ )} +
+ + {({ goBack }) => ( +
+
Step 3: Review
+ + +
+ )} +
+
+ {/* {isConnected ? ( +
+

Evm Address: {address}

+ +
+ ) : ( + + )} */} +
+
+
+ + {!isNil(fiatRampParams) && ( + { + setFiatRampParams(null); + }} + assetKey={fiatRampParams.assetKey} + fiatRampKey={fiatRampParams.fiatRampKey} + /> + )} + { + logEvent([EventName.ProfileModal.buyTokensClicked]); + }} + /> +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/index.ts b/packages/web/components/bridge/immersive/index.ts index 1a8f850f5f..d6609195e5 100644 --- a/packages/web/components/bridge/immersive/index.ts +++ b/packages/web/components/bridge/immersive/index.ts @@ -1 +1 @@ -export * from "./provider"; +export * from "./immersive-bridge"; diff --git a/packages/web/components/bridge/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx similarity index 100% rename from packages/web/components/bridge/more-bridge-options.tsx rename to packages/web/components/bridge/immersive/more-bridge-options.tsx diff --git a/packages/web/components/bridge/immersive/provider.tsx b/packages/web/components/bridge/immersive/provider.tsx deleted file mode 100644 index 4465819b2a..0000000000 --- a/packages/web/components/bridge/immersive/provider.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Transition } from "@headlessui/react"; -import { PropsWithChildren, useState } from "react"; -import { useLockBodyScroll } from "react-use"; - -import { AmountScreen } from "~/components/bridge/amount-screen"; -import { Screen, ScreenManager } from "~/components/screen-manager"; // Import ScreenManager and Screen -import { StepProgress } from "~/components/stepper/progress-bar"; -import { Button } from "~/components/ui/button"; -import { FiatRampKey } from "~/integrations"; - -import { BridgeFlowProvider } from "../flow"; - -export const ImmersiveBridgeFlow = ({ - Provider, - children, -}: PropsWithChildren) => { - const [isVisible, setIsVisible] = useState(false); - const [step, setStep] = useState<0 | 1 | 2>(0); - const [type, setType] = useState<"deposit" | "withdraw">("deposit"); - // const { isConnected, address } = useEvmWalletAccount(); - // const { onOpenWalletSelect } = useWalletSelect(); - // const { disconnect } = useDisconnectEvmWallet(); - - useLockBodyScroll(isVisible); - - return ( - { - setIsVisible(true); - console.log("startBridge", direction); - }, - bridgeAsset: (anyDenom: string, direction: "deposit" | "withdraw") => { - setIsVisible(true); - console.log("bridgeAsset", anyDenom, direction); - }, - fiatRamp: (fiatRampKey: FiatRampKey, assetKey: string) => { - setIsVisible(true); - console.log("fiatRamp", fiatRampKey, assetKey); - }, - fiatRampSelection: () => { - setIsVisible(true); - console.log("fiatRampSelection"); - }, - }} - > - {children} - { - return setStep(Number(screen) as 0 | 1 | 2); - }} - > - -
- - -
- - {({ setCurrentScreen }) => ( -
-
Step 1: Asset
- - -
- )} -
- - {({ setCurrentScreen, goBack }) => ( -
-
- - - -
- - -
- )} -
- - {({ goBack }) => ( -
-
Step 3: Review
- - -
- )} -
-
- {/* {isConnected ? ( -
-

Evm Address: {address}

- -
- ) : ( - - )} */} -
-
-
-
- ); -}; diff --git a/packages/web/components/bridge/legacy.tsx b/packages/web/components/bridge/legacy.tsx index 5b9ea2539d..88148ff273 100644 --- a/packages/web/components/bridge/legacy.tsx +++ b/packages/web/components/bridge/legacy.tsx @@ -17,6 +17,7 @@ import { useTransferConfig, useWindowSize, } from "~/hooks"; +import { BridgeFlowProvider } from "~/hooks/bridge"; import { FiatRampKey } from "~/integrations"; import { ActivateUnverifiedTokenConfirmation, @@ -33,8 +34,6 @@ import { useStore } from "~/stores"; import { UnverifiedAssetsState } from "~/stores/user-settings"; import { removeQueryParam } from "~/utils/url"; -import { BridgeFlowProvider } from "./flow"; - const TransactionTypeQueryParamKey = "transaction_type"; const DenomQueryParamKey = "denom"; @@ -111,14 +110,20 @@ export const LegacyBridgeFlow = observer( ); const startBridge = useCallback( - (direction: "deposit" | "withdraw") => { + ({ direction }: { direction: "deposit" | "withdraw" }) => { transferConfig.startTransfer(direction); }, [transferConfig] ); const bridgeAsset = useCallback( - (anyDenom: string, direction: "deposit" | "withdraw") => { + ({ + anyDenom, + direction, + }: { + anyDenom: string; + direction: "deposit" | "withdraw"; + }) => { const balance = assetsStore.unverifiedIbcBalances.find( ({ balance }) => balance.denom === anyDenom || @@ -160,7 +165,13 @@ export const LegacyBridgeFlow = observer( ); const fiatRamp = useCallback( - (fiatRampKey: FiatRampKey, assetKey: string) => { + ({ + fiatRampKey, + assetKey, + }: { + fiatRampKey: FiatRampKey; + assetKey: string; + }) => { transferConfig.launchFiatRampsModal(fiatRampKey, assetKey); }, [transferConfig] @@ -194,7 +205,7 @@ export const LegacyBridgeFlow = observer( return; } - bridgeAsset(asset.balance.denom, direction); + bridgeAsset({ anyDenom: asset.balance.denom, direction }); removeQueryParam(TransactionTypeQueryParamKey); removeQueryParam(DenomQueryParamKey); }, [router.query, assetsStore.unverifiedIbcBalances, bridgeAsset]); diff --git a/packages/web/components/complex/assets-page-v1.tsx b/packages/web/components/complex/assets-page-v1.tsx index 893c2b156d..f9f91e9cb3 100644 --- a/packages/web/components/complex/assets-page-v1.tsx +++ b/packages/web/components/complex/assets-page-v1.tsx @@ -60,13 +60,13 @@ export const AssetsPageV1: FunctionComponent = observer(() => { { label: t("assets.table.depositButton"), onClick: () => { - startBridge("deposit"); + startBridge({ direction: "deposit" }); }, }, { label: t("assets.table.withdrawButton"), onClick: () => { - startBridge("withdraw"); + startBridge({ direction: "withdraw" }); }, }, ], @@ -75,7 +75,7 @@ export const AssetsPageV1: FunctionComponent = observer(() => { const onTableDeposit = useCallback( (_chainId: string, coinDenom: string, externalDepositUrl?: string) => { if (!externalDepositUrl) { - bridgeAsset(coinDenom, "deposit"); + bridgeAsset({ anyDenom: coinDenom, direction: "deposit" }); } }, [bridgeAsset] @@ -83,7 +83,7 @@ export const AssetsPageV1: FunctionComponent = observer(() => { const onTableWithdraw = useCallback( (_chainId: string, coinDenom: string, externalWithdrawUrl?: string) => { if (!externalWithdrawUrl) { - bridgeAsset(coinDenom, "withdraw"); + bridgeAsset({ anyDenom: coinDenom, direction: "withdraw" }); } }, [bridgeAsset] diff --git a/packages/web/components/complex/portfolio-page.tsx b/packages/web/components/complex/portfolio-page.tsx index c31b262c4e..b25a8dacd2 100644 --- a/packages/web/components/complex/portfolio-page.tsx +++ b/packages/web/components/complex/portfolio-page.tsx @@ -59,13 +59,13 @@ export const PortfolioPage: FunctionComponent = () => { const onDeposit = useCallback( (coinMinimalDenom: string) => { - bridgeAsset(coinMinimalDenom, "deposit"); + bridgeAsset({ anyDenom: coinMinimalDenom, direction: "deposit" }); }, [bridgeAsset] ); const onWithdraw = useCallback( (coinMinimalDenom: string) => { - bridgeAsset(coinMinimalDenom, "withdraw"); + bridgeAsset({ anyDenom: coinMinimalDenom, direction: "withdraw" }); }, [bridgeAsset] ); @@ -173,7 +173,7 @@ const AssetsOverview: FunctionComponent<
+
); })}
diff --git a/packages/web/components/your-balance/your-balance.tsx b/packages/web/components/your-balance/your-balance.tsx index 571eb71704..413f75b36f 100644 --- a/packages/web/components/your-balance/your-balance.tsx +++ b/packages/web/components/your-balance/your-balance.tsx @@ -490,7 +490,9 @@ const BalanceStats = observer(({ denom }: YourBalanceProps) => { @@ -523,7 +525,9 @@ const BalanceStats = observer(({ denom }: YourBalanceProps) => { data.amount.toDec().isZero() } variant="outline" - onClick={() => bridgeAsset(denom, "withdraw")} + onClick={() => + bridgeAsset({ anyDenom: denom, direction: "withdraw" }) + } > {t("assets.historyTable.colums.withdraw")} diff --git a/packages/web/config/generate-lists.ts b/packages/web/config/generate-lists.ts index 98c0b28319..6c7b58d900 100644 --- a/packages/web/config/generate-lists.ts +++ b/packages/web/config/generate-lists.ts @@ -18,6 +18,7 @@ import type { ChainList, IbcTransferMethod, } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; import { generateTsFile } from "~/utils/codegen"; @@ -251,6 +252,26 @@ async function generateAssetListFile({ .join(" | ")}; `; + content += ` + export type ${ + environment === "testnet" + ? "TestnetVariantGroupKeys" + : "MainnetVariantGroupKeys" + } = ${Array.from( + new Set(assetList.assets.map((asset) => asset.variantGroupKey)) + ) + .filter((groupKey, index, self) => { + if (isNil(groupKey)) { + return false; + } + + // remove duplicates + return self.indexOf(groupKey) === index; + }) + .map((groupKey) => `"${groupKey}"`) + .join(" | ")}; + `; + const success = await generateTsFile( content, codegenDir, diff --git a/packages/web/hooks/bridge.tsx b/packages/web/hooks/bridge.tsx index efc4919c0b..b6f6aaf947 100644 --- a/packages/web/hooks/bridge.tsx +++ b/packages/web/hooks/bridge.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from "react"; +import { PropsWithChildren, Provider } from "react"; import { ImmersiveBridgeFlow } from "~/components/bridge/immersive"; import { LegacyBridgeFlow } from "~/components/bridge/legacy"; @@ -9,14 +9,21 @@ import { useFeatureFlags } from "./use-feature-flags"; export type BridgeContext = { /** Start bridging without knowing the asset to bridge yet. */ - startBridge: (direction: "deposit" | "withdraw") => void; + startBridge: (params: { direction: "deposit" | "withdraw" }) => void; /** Start bridging a specified asset of coinMinimalDenom or symbol/denom. */ - bridgeAsset: (anyDenom: string, direction: "deposit" | "withdraw") => void; + bridgeAsset: (params: { + anyDenom: string; + direction: "deposit" | "withdraw"; + }) => void; /** Open a specified fiat on ramp given a specific fiat ramp key and asset key. */ - fiatRamp: (fiatRampKey: FiatRampKey, assetKey: string) => void; + fiatRamp: (params: { fiatRampKey: FiatRampKey; assetKey: string }) => void; /** Open fiat ramp selection. */ fiatRampSelection: () => void; }; +/** A bridge flow for UI for deposit/withdraw or fiat on ramping capable of handling the bridge context provider. */ +export interface BridgeFlowProvider { + Provider: Provider; +} const [BridgeInnerProvider, useBridge] = createContext(); diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index fb90b5bd31..bb4cf0ec80 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -661,7 +661,7 @@ export function useSwapAssets({ Boolean(fromAssetDenom) && Boolean(toAssetDenom) && useOtherCurrencies; - // use a separate query for search to maintain pagination in other infinite query + const { data: selectableAssetPages, isLoading: isLoadingSelectAssets, diff --git a/packages/web/modals/fiat-on-ramp-selection.tsx b/packages/web/modals/fiat-on-ramp-selection.tsx index 0198adc963..29c591eecf 100644 --- a/packages/web/modals/fiat-on-ramp-selection.tsx +++ b/packages/web/modals/fiat-on-ramp-selection.tsx @@ -58,7 +58,7 @@ export const FiatOnrampSelectionModal: FunctionComponent< className="flex h-28 items-center !justify-start gap-2 !bg-osmoverse-900 px-5 py-5 transition-colors hover:!bg-osmoverse-700" onClick={() => { onSelectRamp?.(rampKey); - fiatRamp(rampKey, initialAsset); + fiatRamp({ fiatRampKey: rampKey, assetKey: initialAsset }); modalProps.onRequestClose(); }} > From 252e0d067286433a0dfc6f1a1096a95435f6e043 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Wed, 19 Jun 2024 20:05:58 -0400 Subject: [PATCH 02/33] feat: display selected asset --- .../bridge/immersive/amount-screen.tsx | 653 +++++++++--------- .../bridge/immersive/immersive-bridge.tsx | 181 +++-- 2 files changed, 415 insertions(+), 419 deletions(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 6d71f0a3cb..a98b542f9a 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,6 +1,7 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import { MinimalAsset } from "@osmosis-labs/types"; import { isNumeric } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; @@ -15,368 +16,378 @@ import { Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; import { useTranslation } from "~/hooks"; +import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; import { trimPlaceholderZeros } from "~/utils/number"; import { api } from "~/utils/trpc"; interface AmountScreenProps { type: "deposit" | "withdraw"; + assetInOsmosis: MinimalAsset; } -export const AmountScreen = observer(({ type }: AmountScreenProps) => { - const { accountStore } = useStore(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); - const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); - const { t } = useTranslation(); - - const { data: asset, isLoading } = api.edge.assets.getAssetWithPrice.useQuery( - { - findMinDenomOrSymbol: "USDC", +export const AmountScreen = observer( + ({ type, assetInOsmosis }: AmountScreenProps) => { + const { accountStore } = useStore(); + const wallet = accountStore.getWallet(accountStore.osmosisChainId); + const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); + const { t } = useTranslation(); + + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ + findChainNameOrId: "osmosis", + }); + const { data: nobleChain } = api.edge.chains.getChain.useQuery({ + findChainNameOrId: "noble", + }); + + const { price: assetInOsmosisPrice, isLoading } = usePrice(assetInOsmosis); + + const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); + const [cryptoAmount, setCryptoAmount] = useState("0"); + const [fiatAmount, setFiatAmount] = useState("0"); + + // TODO: Add skeleton loaders + if ( + isLoading || + !assetInOsmosis || + !assetInOsmosisPrice || + !osmosisChain || + !nobleChain + ) { + return null; } - ); - const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "osmosis", - }); - const { data: nobleChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "noble", - }); - - const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); - const [cryptoAmount, setCryptoAmount] = useState("0"); - const [fiatAmount, setFiatAmount] = useState("0"); - - if (isLoading || !asset || !osmosisChain || !nobleChain) { - return null; - } - - const cryptoAmountPretty = new CoinPretty( - asset, - cryptoAmount === "" ? new Dec(0) : cryptoAmount - ); - const fiatAmountPretty = new PricePretty( - DEFAULT_VS_CURRENCY, - new Dec(fiatAmount === "" ? 0 : fiatAmount) - ); - const parseFiatAmount = (value: string) => { - return value.replace("$", ""); - }; - - const formatFiatAmount = (value: string) => { - return `$${value}`; - }; - - const onInput = (type: "fiat" | "crypto") => (value: string) => { - let nextValue = type === "fiat" ? parseFiatAmount(value) : value; - if (!isNumeric(nextValue) && nextValue !== "") return; - - if (nextValue.startsWith("0") && !nextValue.startsWith("0.")) { - nextValue = nextValue.slice(1); - } - if (nextValue === "") { - nextValue = "0"; - } - if (nextValue === ".") { - nextValue = "0."; - } - - if (type === "fiat") { - // Update the crypto amount based on the fiat amount - const priceInFiat = asset.currentPrice.toDec(); - const nextFiatAmount = new Dec(nextValue); - const nextCryptoAmount = nextFiatAmount - .quo(priceInFiat) - .mul(DecUtils.getTenExponentN(asset.coinDecimals)) - .toString(); - - setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); - } else { - // Update the fiat amount based on the crypto amount - const priceInFiat = asset.currentPrice.toDec(); - const nextCryptoAmount = new Dec(nextValue); - const nextFiatAmount = nextCryptoAmount.mul(priceInFiat).toString(); - - setFiatAmount(trimPlaceholderZeros(nextFiatAmount)); - } + const cryptoAmountPretty = new CoinPretty( + assetInOsmosis, + cryptoAmount === "" ? new Dec(0) : cryptoAmount + ); + const fiatAmountPretty = new PricePretty( + DEFAULT_VS_CURRENCY, + new Dec(fiatAmount === "" ? 0 : fiatAmount) + ); - type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); - }; + const parseFiatAmount = (value: string) => { + return value.replace("$", ""); + }; + + const formatFiatAmount = (value: string) => { + return `$${value}`; + }; + + const onInput = (type: "fiat" | "crypto") => (value: string) => { + let nextValue = type === "fiat" ? parseFiatAmount(value) : value; + if (!isNumeric(nextValue) && nextValue !== "") return; + + if (nextValue.startsWith("0") && !nextValue.startsWith("0.")) { + nextValue = nextValue.slice(1); + } + if (nextValue === "") { + nextValue = "0"; + } + if (nextValue === ".") { + nextValue = "0."; + } + + if (type === "fiat") { + // Update the crypto amount based on the fiat amount + const priceInFiat = assetInOsmosisPrice.toDec(); + const nextFiatAmount = new Dec(nextValue); + const nextCryptoAmount = nextFiatAmount + .quo(priceInFiat) + .mul(DecUtils.getTenExponentN(assetInOsmosis.coinDecimals)) + .toString(); + + setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); + } else { + // Update the fiat amount based on the crypto amount + const priceInFiat = assetInOsmosisPrice.toDec(); + const nextCryptoAmount = new Dec(nextValue); + const nextFiatAmount = nextCryptoAmount.mul(priceInFiat).toString(); + + setFiatAmount(trimPlaceholderZeros(nextFiatAmount)); + } + + type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); + }; - return ( -
-
- - {type === "deposit" ? t("transfer.deposit") : t("transfer.withdraw")} - {" "} - token image{" "} - {asset.coinDenom} -
- -
-
- - {t("transfer.fromNetwork")} - - - - {t("transfer.toNetwork")} - -
+ return ( +
+
+ + {type === "deposit" + ? t("transfer.deposit") + : t("transfer.withdraw")} + {" "} + token image{" "} + {assetInOsmosis.coinDenom} +
+ +
+
+ + {t("transfer.fromNetwork")} + + + + {t("transfer.toNetwork")} + +
-
- - {nobleChain.pretty_name} - +
+ + {nobleChain.pretty_name} + - + - - {osmosisChain.pretty_name} - + + {osmosisChain.pretty_name} + +
-
-
-
-
-
- {inputUnit === "fiat" ? ( - <> - - - ) : ( - <> - - {cryptoAmountPretty?.denom} - - } - /> - - )} -
- -
- - -
+
+ +
-
- {[ - { - label: "USDC.e", - amount: `$80.00 ${t("transfer.available")}`, - active: true, - }, - { label: "USDC", amount: "$30.00", active: false }, - { label: "USDC.axl", amount: "$10.00", active: false }, - ].map(({ label, amount, active }, index) => ( - - ))} -
+
-
- - {type === "deposit" - ? t("transfer.transferWith") - : t("transfer.transferTo")} - -
- {wallet?.walletInfo.prettyName} - {wallet?.walletInfo.prettyName} - +
+ {[ + { + label: "USDC.e", + amount: `$80.00 ${t("transfer.available")}`, + active: true, + }, + { label: "USDC", amount: "$30.00", active: false }, + { label: "USDC.axl", amount: "$10.00", active: false }, + ].map(({ label, amount, active }, index) => ( + + ))}
-
- - {({ open }) => ( -
- -
-
- - {t("transfer.receiveAsset")} - - -

- {t("transfer.receiveAsset")} -

-

- {t("transfer.receiveAssetDescription")} -

-
- } - > - - -
+
+ + {type === "deposit" + ? t("transfer.transferWith") + : t("transfer.transferTo")} + +
+ {wallet?.walletInfo.prettyName} + {wallet?.walletInfo.prettyName} + +
+
-
- USDC - + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

+ {t("transfer.receiveAsset")} +

+

+ {t("transfer.receiveAssetDescription")} +

+
} - )} - /> -
-
- - - - - - - - - - +
+
+ + + + + + + + + +
+ )} +
+ +
+
+ + + {t("transfer.estimatingTime")} +
- )} - -
-
- - {t("transfer.estimatingTime")} + {t("transfer.calculatingFees")}
- - {t("transfer.calculatingFees")} - -
- -
- - - setIsMoreOptionsVisible(false)} - /> +
+ + + setIsMoreOptionsVisible(false)} + /> +
-
- ); -}); + ); + } +); const ChainSelectorButton: FunctionComponent<{ readonly?: boolean; diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index e1a3eb0783..841f553bf2 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -1,5 +1,5 @@ import { Transition } from "@headlessui/react"; -import { BridgeAsset } from "@osmosis-labs/bridge"; +import { MinimalAsset } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; import { PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; @@ -38,7 +38,7 @@ export const ImmersiveBridgeFlow = ({ const apiUtils = api.useUtils(); - const [osmosisAsset, setOsmosisAsset] = useState(); + const [assetInOsmosis, setAssetInOsmosis] = useState(); const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; @@ -59,7 +59,7 @@ export const ImmersiveBridgeFlow = ({ const onClose = () => { setIsVisible(false); - setOsmosisAsset(undefined); + setAssetInOsmosis(undefined); setStep(ImmersiveBridgeScreens.Asset); }; @@ -113,12 +113,7 @@ export const ImmersiveBridgeFlow = ({ return; } - setOsmosisAsset({ - address: asset.coinMinimalDenom, - decimals: asset.coinDecimals, - denom: asset.coinDenom, - sourceDenom: asset.sourceDenom, - }); + setAssetInOsmosis(asset); console.log("bridgeAsset", anyDenom, direction); }, fiatRamp: ({ @@ -140,97 +135,86 @@ export const ImmersiveBridgeFlow = ({ return setStep(screen as ImmersiveBridgeScreens); }} > - - onClose()} /> + {() => ( + + onClose()} /> + {/* { + setStep(nextScreen); + }} + className={ + "absolute left-8 top-[28px] z-50 w-fit text-osmoverse-400 hover:text-osmoverse-100" + } + icon={} + aria-label="Go Back" + /> */} -
- setStep(ImmersiveBridgeScreens.Asset) - : undefined, - }, - { - displayLabel: "Amount", - onClick: - step === ImmersiveBridgeScreens.Review - ? () => setStep(ImmersiveBridgeScreens.Amount) - : undefined, - }, - { - displayLabel: "Review", - }, - ]} - currentStep={Number(step)} - /> +
+ setStep(ImmersiveBridgeScreens.Asset) + : undefined, + }, + { + displayLabel: "Amount", + onClick: + step === ImmersiveBridgeScreens.Review + ? () => setStep(ImmersiveBridgeScreens.Amount) + : undefined, + }, + { + displayLabel: "Review", + }, + ]} + currentStep={Number(step)} + /> -
- - {({ setCurrentScreen }) => ( - { - setCurrentScreen(ImmersiveBridgeScreens.Amount); - setOsmosisAsset({ - address: asset.coinMinimalDenom, - decimals: asset.coinDecimals, - denom: asset.coinDenom, - sourceDenom: asset.sourceDenom, - }); - }} - /> - )} - - - {({ setCurrentScreen, goBack }) => ( -
-
+
+ + {({ setCurrentScreen }) => ( + { + setCurrentScreen(ImmersiveBridgeScreens.Amount); + setAssetInOsmosis(asset); + }} + /> + )} + + + {() => ( + + )} + + + {({ goBack }) => ( +
+
Step 3: Review
- - +
- - -
- )} - - - {({ goBack }) => ( -
-
Step 3: Review
- - -
- )} -
-
- {/* {isConnected ? ( + )} + +
+ {/* {isConnected ? (

Evm Address: {address}

@@ -251,8 +235,9 @@ export const ImmersiveBridgeFlow = ({ Connect EVM Wallet )} */} -
- +
+ + )} {!isNil(fiatRampParams) && ( From 9b462f8c1ab1b14073b619aaa3a9e8797fa39a36 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 20 Jun 2024 17:34:01 -0400 Subject: [PATCH 03/33] feat: add skeleton loader to amount screen --- packages/server/src/utils/pagination.ts | 13 ++++--- packages/trpc/src/assets.ts | 12 ++++-- .../bridge/immersive/amount-screen.tsx | 37 +++++++++++++------ ...ets-screen.tsx => asset-select-screen.tsx} | 16 ++++++-- .../immersive/bridge-network-select.tsx | 7 +++- .../bridge/immersive/immersive-bridge.tsx | 21 +++++++---- .../bridge/immersive/more-bridge-options.tsx | 4 +- packages/web/components/input/input-box.tsx | 15 +++++++- packages/web/localizations/en.json | 9 +++++ 9 files changed, 96 insertions(+), 38 deletions(-) rename packages/web/components/bridge/immersive/{assets-screen.tsx => asset-select-screen.tsx} (94%) diff --git a/packages/server/src/utils/pagination.ts b/packages/server/src/utils/pagination.ts index ab3171a9b0..b4b30e9ed9 100644 --- a/packages/server/src/utils/pagination.ts +++ b/packages/server/src/utils/pagination.ts @@ -32,23 +32,24 @@ export function maybeCursorPaginatedItems( limit: number | null | undefined ): { items: TItem[]; - nextCursor: number | null; + nextCursor: number | undefined; } { - if (!cursor && !limit) return { items, nextCursor: null }; + if (!cursor && !limit) return { items, nextCursor: undefined }; cursor = cursor || 0; limit = limit || 50; const startIndex = cursor; // no more items if given an invalid cursor - if (startIndex > items.length - 1) return { items: [], nextCursor: null }; + if (startIndex > items.length - 1) + return { items: [], nextCursor: undefined }; // get the page const page = items.slice(startIndex, startIndex + limit); return { items: page, - nextCursor: cursor + limit > items.length - 1 ? null : cursor + limit, + nextCursor: cursor + limit > items.length - 1 ? undefined : cursor + limit, }; } @@ -73,11 +74,11 @@ export async function maybeCachePaginatedItems({ ttl = 30 * 1000, // 30 seconds }: CachedPaginationParams): Promise<{ items: TItem[]; - nextCursor: number | null; + nextCursor: number | undefined; }> { // if pagination is not used, return items if (!cursor && !limit) - return { items: await getFreshItems(), nextCursor: null }; + return { items: await getFreshItems(), nextCursor: undefined }; // If cursor is 0, delete the cache entry for the given key if (cursor === 0) { diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index 81e02024ab..8768bee846 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -540,9 +540,15 @@ export const assetsRouter = createTRPCRouter({ }); if (type === "withdraw") { - assets = assets - // Filter out all assets without amount - .filter((asset) => !isNil(asset.amount)); + const hasBalance = assets.some((asset) => + asset.amount?.toDec().isPositive() + ); + + assets = hasBalance + ? assets + // Filter out all assets without amount + .filter((asset) => !isNil(asset.amount)) + : assets; // display all assets if no balance } if (type === "deposit") { diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index a98b542f9a..b71c5b5f5e 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -12,7 +12,7 @@ import { Icon } from "~/components/assets"; import { BridgeNetworkSelect } from "~/components/bridge/immersive/bridge-network-select"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; import { InputBox } from "~/components/input"; -import { Spinner } from "~/components/loaders"; +import { SkeletonLoader, Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; import { useTranslation } from "~/hooks"; @@ -54,7 +54,7 @@ export const AmountScreen = observer( !osmosisChain || !nobleChain ) { - return null; + return ; } const cryptoAmountPretty = new CoinPretty( @@ -152,15 +152,15 @@ export const AmountScreen = observer(
-
-
-
+
+
+
{inputUnit === "fiat" ? ( <> @@ -171,11 +171,11 @@ export const AmountScreen = observer( currentValue={cryptoAmount} onInput={onInput("crypto")} className="border-none bg-transparent text-center" - trailingSymbol={ - - {cryptoAmountPretty?.denom} - - } + classes={{ + trailingSymbol: + "ml-1 align-middle text-2xl text-osmoverse-500 w-full text-left", + }} + trailingSymbol={cryptoAmountPretty?.denom} /> )} @@ -427,3 +427,18 @@ const ChainSelectorButton: FunctionComponent<{ ); }; + +const AmountScreenSkeletonLoader = () => { + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/assets-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx similarity index 94% rename from packages/web/components/bridge/immersive/assets-screen.tsx rename to packages/web/components/bridge/immersive/asset-select-screen.tsx index fe0922f4cf..26b6aabca5 100644 --- a/packages/web/components/bridge/immersive/assets-screen.tsx +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -39,13 +39,13 @@ const prioritizedDenoms = [ "TIA", ] satisfies (MainnetAssetSymbols | TestnetAssetSymbols)[]; -interface AssetsScreenProps { +interface AssetSelectScreenProps { type: "deposit" | "withdraw"; onSelectAsset: (asset: MinimalAsset) => void; } -export const AssetsScreen = observer( - ({ type, onSelectAsset }: AssetsScreenProps) => { +export const AssetSelectScreen = observer( + ({ type, onSelectAsset }: AssetSelectScreenProps) => { const { accountStore, userSettings } = useStore(); const { showPreviewAssets } = useShowPreviewAssets(); const { t } = useTranslation(); @@ -116,12 +116,20 @@ export const AssetsScreen = observer( }} /> +

+ {t( + type === "deposit" + ? "transfer.assetSelectScreen.titleDeposit" + : "transfer.assetSelectScreen.titleWithdraw" + )} +

+ { setSearch(nextValue); }, 300)} className="my-4 flex-shrink-0" - placeholder="Search assets" + placeholder={t("transfer.assetSelectScreen.searchAssets")} size="full" /> diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index 650d38dc74..6368552f44 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -4,10 +4,13 @@ import React, { useMemo, useState } from "react"; import { SearchBox } from "~/components/input"; import { Intersection } from "~/components/intersection"; import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; import { api } from "~/utils/trpc"; export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(""); const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = api.edge.chains.getChains.useInfiniteQuery( @@ -37,7 +40,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { return ( @@ -47,7 +50,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { setQuery(nextValue); }, 300)} className="my-4 flex-shrink-0" - placeholder="Search supported networks" + placeholder={t("bridgeNetworkSelect.searchPlaceholder")} size="full" />
diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 841f553bf2..87a4c6492f 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -1,11 +1,11 @@ import { Transition } from "@headlessui/react"; import { MinimalAsset } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; -import { PropsWithChildren, useState } from "react"; +import { memo, PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; -import { AssetsScreen } from "~/components/bridge/immersive/assets-screen"; +import { AssetSelectScreen } from "~/components/bridge/immersive/asset-select-screen"; import { Screen, ScreenManager } from "~/components/screen-manager"; import { StepProgress } from "~/components/stepper/progress-bar"; import { Button } from "~/components/ui/button"; @@ -25,6 +25,10 @@ enum ImmersiveBridgeScreens { Review = "2", } +const MemoizedChildren = memo(({ children }: PropsWithChildren<{}>) => { + return <>{children}; +}); + export const ImmersiveBridgeFlow = ({ Provider, children, @@ -59,8 +63,6 @@ export const ImmersiveBridgeFlow = ({ const onClose = () => { setIsVisible(false); - setAssetInOsmosis(undefined); - setStep(ImmersiveBridgeScreens.Asset); }; const onOpen = (direction: "deposit" | "withdraw") => { @@ -73,7 +75,6 @@ export const ImmersiveBridgeFlow = ({ value={{ startBridge: ({ direction }: { direction: "deposit" | "withdraw" }) => { onOpen(direction); - console.log("startBridge", direction); }, bridgeAsset: async ({ anyDenom, @@ -83,6 +84,7 @@ export const ImmersiveBridgeFlow = ({ direction: "deposit" | "withdraw"; }) => { onOpen(direction); + setStep(ImmersiveBridgeScreens.Amount); const fetchAssetWithRetry = async (retries = 3) => { for (let attempt = 1; attempt <= retries; attempt++) { @@ -114,7 +116,6 @@ export const ImmersiveBridgeFlow = ({ } setAssetInOsmosis(asset); - console.log("bridgeAsset", anyDenom, direction); }, fiatRamp: ({ fiatRampKey, @@ -128,7 +129,7 @@ export const ImmersiveBridgeFlow = ({ fiatRampSelection: onOpenFiatOnrampSelection, }} > - {children} + {children} { @@ -146,6 +147,10 @@ export const ImmersiveBridgeFlow = ({ leave="transition-opacity duration-150" leaveFrom="opacity-100" leaveTo="opacity-0" + afterLeave={() => { + setAssetInOsmosis(undefined); + setStep(ImmersiveBridgeScreens.Asset); + }} > onClose()} /> {/* {({ setCurrentScreen }) => ( - { setCurrentScreen(ImmersiveBridgeScreens.Amount); diff --git a/packages/web/components/bridge/immersive/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx index 54abbe5e17..e58640d090 100644 --- a/packages/web/components/bridge/immersive/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -66,7 +66,7 @@ export const MoreBridgeOptions = observer( className="!max-w-[30rem]" {...modalProps} > -

+

{t( type === "deposit" ? "transfer.moreBridgeOptions.descriptionDeposit" @@ -76,7 +76,7 @@ export const MoreBridgeOptions = observer( chain: prettyChainName, } )} -

+

{isLoadingExternalUrls || isLoadingAsset ? ( <> diff --git a/packages/web/components/input/input-box.tsx b/packages/web/components/input/input-box.tsx index 37fba3aa67..5d44ecd419 100644 --- a/packages/web/components/input/input-box.tsx +++ b/packages/web/components/input/input-box.tsx @@ -14,6 +14,8 @@ export interface Button extends ButtonProps, CustomClasses, Disableable { label: string; } +type ClassVariants = "label" | "input" | "trailingSymbol"; + interface Props extends Optional, "currentValue">, Disableable, @@ -29,9 +31,11 @@ interface Props clearButton?: boolean; /** Display a symbol after the input box, ex: '%'. */ trailingSymbol?: React.ReactNode; + /** @deprecated Use 'classes' instead */ inputClassName?: string; isAutosize?: boolean; inputRef?: React.MutableRefObject; + classes?: Partial>; } export const InputBox: FunctionComponent = ({ @@ -46,6 +50,7 @@ export const InputBox: FunctionComponent = ({ labelButtons = [], trailingSymbol, inputClassName, + classes, disabled = false, className, isAutosize, @@ -77,7 +82,10 @@ export const InputBox: FunctionComponent = ({ )} >
{!rightEntry && diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index c045d92234..08e903071d 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Choose from the following alternative providers to withdraw (aka “bridge”) your {asset} to {chain}.", "depositWith": "Deposit with", "withdrawWith": "Withdraw with" + }, + "assetSelectScreen": { + "titleDeposit": "Select an asset to deposit", + "titleWithdraw": "Select an asset to withdraw", + "searchAssets": "Search assets" + }, + "bridgeNetworkSelect": { + "title": "Select network", + "searchPlaceholder": "Search supported networks" } }, "unknownError": "Unknown error", From 681c4e3aa04268804f29c22cb533b5187793771f Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 20 Jun 2024 18:23:35 -0400 Subject: [PATCH 04/33] feat: handle back button, and improve amount input --- .../bridge/immersive/amount-screen.tsx | 147 ++++++++++-------- .../immersive/bridge-network-select.tsx | 4 +- .../bridge/immersive/immersive-bridge.tsx | 27 ++-- 3 files changed, 102 insertions(+), 76 deletions(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index b71c5b5f5e..e6f26cf916 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -2,7 +2,7 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { MinimalAsset } from "@osmosis-labs/types"; -import { isNumeric } from "@osmosis-labs/utils"; +import { isNumeric, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; @@ -15,7 +15,7 @@ import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; -import { useTranslation } from "~/hooks"; +import { useConnectWalletModalRedirect, useTranslation } from "~/hooks"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; import { trimPlaceholderZeros } from "~/utils/number"; @@ -33,6 +33,14 @@ export const AmountScreen = observer( const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); const { t } = useTranslation(); + const { accountActionButton: connectWalletButton, walletConnected } = + useConnectWalletModalRedirect( + { + className: "w-full", + }, + noop + ); + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ findChainNameOrId: "osmosis", }); @@ -59,8 +67,13 @@ export const AmountScreen = observer( const cryptoAmountPretty = new CoinPretty( assetInOsmosis, - cryptoAmount === "" ? new Dec(0) : cryptoAmount + cryptoAmount === "" + ? new Dec(0) + : new Dec(cryptoAmount).mul( + DecUtils.getTenExponentN(assetInOsmosis.coinDecimals) + ) ); + const fiatAmountPretty = new PricePretty( DEFAULT_VS_CURRENCY, new Dec(fiatAmount === "" ? 0 : fiatAmount) @@ -92,10 +105,7 @@ export const AmountScreen = observer( // Update the crypto amount based on the fiat amount const priceInFiat = assetInOsmosisPrice.toDec(); const nextFiatAmount = new Dec(nextValue); - const nextCryptoAmount = nextFiatAmount - .quo(priceInFiat) - .mul(DecUtils.getTenExponentN(assetInOsmosis.coinDecimals)) - .toString(); + const nextCryptoAmount = nextFiatAmount.quo(priceInFiat).toString(); setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); } else { @@ -165,53 +175,58 @@ export const AmountScreen = observer( /> ) : ( - <> +
+

+ {cryptoAmountPretty?.denom} +

- +
)}
- -
- +
+ + + +
+
@@ -363,25 +378,31 @@ export const AmountScreen = observer(
- - - setIsMoreOptionsVisible(false)} - /> + {!walletConnected ? ( + connectWalletButton + ) : ( + <> + + + setIsMoreOptionsVisible(false)} + /> + + )}
diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index 6368552f44..f0ad0efb95 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -40,7 +40,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { return ( @@ -50,7 +50,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { setQuery(nextValue); }, 300)} className="my-4 flex-shrink-0" - placeholder={t("bridgeNetworkSelect.searchPlaceholder")} + placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} size="full" />
diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 87a4c6492f..9b18ff5839 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -4,11 +4,12 @@ import { isNil } from "@osmosis-labs/utils"; import { memo, PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; +import { Icon } from "~/components/assets"; import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; import { AssetSelectScreen } from "~/components/bridge/immersive/asset-select-screen"; import { Screen, ScreenManager } from "~/components/screen-manager"; import { StepProgress } from "~/components/stepper/progress-bar"; -import { Button } from "~/components/ui/button"; +import { Button, IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; import { BridgeFlowProvider } from "~/hooks/bridge"; import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; @@ -153,16 +154,20 @@ export const ImmersiveBridgeFlow = ({ }} > onClose()} /> - {/* { - setStep(nextScreen); - }} - className={ - "absolute left-8 top-[28px] z-50 w-fit text-osmoverse-400 hover:text-osmoverse-100" - } - icon={} - aria-label="Go Back" - /> */} + {step !== ImmersiveBridgeScreens.Asset && ( + { + const previousStep = Number(step) - 1; + // @ts-expect-error + setStep(previousStep); + }} + className={ + "absolute left-8 top-[28px] z-50 w-fit text-osmoverse-400 hover:text-osmoverse-100" + } + icon={} + aria-label="Go Back" + /> + )}
Date: Thu, 20 Jun 2024 18:25:44 -0400 Subject: [PATCH 05/33] fix: pagination test --- packages/server/src/utils/__tests__/pagination.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/__tests__/pagination.spec.ts b/packages/server/src/utils/__tests__/pagination.spec.ts index 71feac1e98..a33a3b4081 100644 --- a/packages/server/src/utils/__tests__/pagination.spec.ts +++ b/packages/server/src/utils/__tests__/pagination.spec.ts @@ -6,7 +6,7 @@ describe("maybeCursorPaginatedItems", () => { test("returns all items if no cursor and limit are provided", () => { const result = maybeCursorPaginatedItems(items, null, null); expect(result.items).toEqual(items); - expect(result.nextCursor).toEqual(null); // null since no next page + expect(result.nextCursor).toEqual(undefined); // null since no next page }); test("returns first page of items with default limit", () => { @@ -24,13 +24,13 @@ describe("maybeCursorPaginatedItems", () => { test("returns last items of page items based on provided cursor and limit", () => { const result = maybeCursorPaginatedItems(items, 90, 20); expect(result.items).toEqual(items.slice(90, 100)); - expect(result.nextCursor).toEqual(null); + expect(result.nextCursor).toEqual(undefined); }); test("returns elements that are less then a single page size", () => { const lessItems = [1, 2, 3]; const result = maybeCursorPaginatedItems(lessItems, 0, 20); expect(result.items).toEqual(lessItems); - expect(result.nextCursor).toEqual(null); + expect(result.nextCursor).toEqual(undefined); }); }); From 39124f21b9e202dfad2d4186b0fcb8db925f358d Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 20 Jun 2024 18:36:09 -0400 Subject: [PATCH 06/33] fix: type --- packages/web/hooks/use-swap.tsx | 18 +++++++++--------- packages/web/localizations/de.json | 9 +++++++++ packages/web/localizations/es.json | 9 +++++++++ packages/web/localizations/fa.json | 9 +++++++++ packages/web/localizations/fr.json | 9 +++++++++ packages/web/localizations/gu.json | 9 +++++++++ packages/web/localizations/hi.json | 9 +++++++++ packages/web/localizations/ja.json | 9 +++++++++ packages/web/localizations/ko.json | 9 +++++++++ packages/web/localizations/pl.json | 9 +++++++++ packages/web/localizations/pt-br.json | 9 +++++++++ packages/web/localizations/ro.json | 9 +++++++++ packages/web/localizations/ru.json | 9 +++++++++ packages/web/localizations/tr.json | 9 +++++++++ packages/web/localizations/zh-cn.json | 9 +++++++++ packages/web/localizations/zh-hk.json | 9 +++++++++ packages/web/localizations/zh-tw.json | 9 +++++++++ 17 files changed, 153 insertions(+), 9 deletions(-) diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index bb4cf0ec80..173af8498d 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -5,13 +5,13 @@ import { NotEnoughLiquidityError, NotEnoughQuotedError, } from "@osmosis-labs/pools"; -import type { Asset, RouterKey } from "@osmosis-labs/server"; +import type { RouterKey } from "@osmosis-labs/server"; import { makeSplitRoutesSwapExactAmountInMsg, makeSwapExactAmountInMsg, SignOptions, } from "@osmosis-labs/stores"; -import { Currency } from "@osmosis-labs/types"; +import { Currency, MinimalAsset } from "@osmosis-labs/types"; import { getAssetFromAssetList, isNil, @@ -961,7 +961,7 @@ function useToFromDenoms({ /** Will query for an individual asset of any type of denom (symbol, min denom) * if it's not already in the list of existing assets. */ -function useSwapAsset({ +function useSwapAsset({ minDenomOrSymbol, existingAssets = [], }: { @@ -1008,12 +1008,12 @@ function getSwapTxParameters({ quote: | RouterOutputs["local"]["quoteRouter"]["routeTokenOutGivenIn"] | undefined; - fromAsset: Asset & + fromAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - toAsset: Asset & + toAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; @@ -1095,12 +1095,12 @@ function getSwapMessages({ quote: | RouterOutputs["local"]["quoteRouter"]["routeTokenOutGivenIn"] | undefined; - fromAsset: Asset & + fromAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - toAsset: Asset & + toAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; @@ -1156,12 +1156,12 @@ function useQueryRouterBestQuote( RouterInputs["local"]["quoteRouter"]["routeTokenOutGivenIn"], "preferredRouter" | "tokenInDenom" | "tokenOutDenom" > & { - tokenIn: Asset & + tokenIn: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - tokenOut: Asset & + tokenOut: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 5afb2a8e77..309f6e8f44 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Wählen Sie aus den folgenden alternativen Anbietern zum Abheben (auch „Brückenübertragen“) Ihres {asset} auf {chain} .", "depositWith": "Einzahlung mit", "withdrawWith": "Abheben mit" + }, + "assetSelectScreen": { + "titleDeposit": "Wählen Sie einen Vermögenswert zur Einzahlung aus", + "titleWithdraw": "Wählen Sie einen Vermögenswert zum Abheben aus", + "searchAssets": "Assets suchen" + }, + "bridgeNetworkSelect": { + "title": "Netzwerk auswählen", + "searchPlaceholder": "Unterstützte Netzwerke durchsuchen" } }, "unknownError": "Unbekannter Fehler", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index fd97f73ece..d6ff837080 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Elija entre los siguientes proveedores alternativos para retirar (también conocido como “puente”) su {asset} a {chain} .", "depositWith": "Depositar con", "withdrawWith": "Retirarse con" + }, + "assetSelectScreen": { + "titleDeposit": "Seleccione un activo para depositar", + "titleWithdraw": "Seleccione un activo para retirar", + "searchAssets": "Buscar activos" + }, + "bridgeNetworkSelect": { + "title": "Seleccione la red", + "searchPlaceholder": "Buscar redes compatibles" } }, "unknownError": "Error desconocido", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 5fb244f3b4..609a2a3d39 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "از میان ارائه‌دهندگان جایگزین زیر {asset} خود را به {chain} .", "depositWith": "سپرده گذاری با", "withdrawWith": "برداشت با" + }, + "assetSelectScreen": { + "titleDeposit": "دارایی را برای سپرده گذاری انتخاب کنید", + "titleWithdraw": "دارایی را برای برداشت انتخاب کنید", + "searchAssets": "جستجوی دارایی ها" + }, + "bridgeNetworkSelect": { + "title": "شبکه را انتخاب کنید", + "searchPlaceholder": "جستجوی شبکه های پشتیبانی شده" } }, "unknownError": "خطای نا شناس", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index fbeeecade7..b26d8a5416 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Choisissez parmi les fournisseurs alternatifs suivants pour retirer (alias « pont ») votre {asset} vers {chain} .", "depositWith": "Dépôt avec", "withdrawWith": "Retirer avec" + }, + "assetSelectScreen": { + "titleDeposit": "Sélectionnez un actif à déposer", + "titleWithdraw": "Sélectionnez un actif à retirer", + "searchAssets": "Rechercher des ressources" + }, + "bridgeNetworkSelect": { + "title": "Sélectionnez réseau", + "searchPlaceholder": "Rechercher des réseaux pris en charge" } }, "unknownError": "Erreur inconnue", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index a5308192d4..ac2b053206 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "તમારા {asset} થી {chain} ઉપાડવા માટે નીચેના વૈકલ્પિક પ્રદાતાઓમાંથી પસંદ કરો (ઉર્ફે \"બ્રિજ\"). .", "depositWith": "સાથે જમા", "withdrawWith": "સાથે પાછી ખેંચો" + }, + "assetSelectScreen": { + "titleDeposit": "જમા કરવા માટે સંપત્તિ પસંદ કરો", + "titleWithdraw": "ઉપાડવા માટે સંપત્તિ પસંદ કરો", + "searchAssets": "સંપત્તિ શોધો" + }, + "bridgeNetworkSelect": { + "title": "નેટવર્ક પસંદ કરો", + "searchPlaceholder": "સપોર્ટેડ નેટવર્ક્સ શોધો" } }, "unknownError": "અજાણી ભૂલ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 6e092a9717..6422f07654 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "अपने {asset} को {chain} वापस लेने (उर्फ “ब्रिज”) के लिए निम्नलिखित वैकल्पिक प्रदाताओं में से चुनें।", "depositWith": "जमा करें", "withdrawWith": "साथ वापस लें" + }, + "assetSelectScreen": { + "titleDeposit": "जमा करने के लिए एक परिसंपत्ति का चयन करें", + "titleWithdraw": "निकासी के लिए एक परिसंपत्ति का चयन करें", + "searchAssets": "संपत्ति खोजें" + }, + "bridgeNetworkSelect": { + "title": "नेटवर्क चुनें", + "searchPlaceholder": "समर्थित नेटवर्क खोजें" } }, "unknownError": "अज्ञात त्रुटि", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index d18b3e8512..683552416f 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "次の代替プロバイダーから選択して{asset}を{chain} 。", "depositWith": "入金", "withdrawWith": "撤退する" + }, + "assetSelectScreen": { + "titleDeposit": "預ける資産を選択してください", + "titleWithdraw": "引き出す資産を選択してください", + "searchAssets": "アセットを検索" + }, + "bridgeNetworkSelect": { + "title": "ネットワークを選択", + "searchPlaceholder": "サポートされているネットワークを検索" } }, "unknownError": "不明なエラー", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 89de3a7a2a..9475f50ea0 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "귀하의 {asset} {chain} 으로 철회(일명 \"브리지\")하려면 다음 대체 제공자 중에서 선택하세요. .", "depositWith": "다음으로 입금", "withdrawWith": "다음으로 인출" + }, + "assetSelectScreen": { + "titleDeposit": "입금할 자산을 선택하세요", + "titleWithdraw": "출금할 자산을 선택하세요", + "searchAssets": "자산 검색" + }, + "bridgeNetworkSelect": { + "title": "네트워크 선택", + "searchPlaceholder": "지원되는 네트워크 검색" } }, "unknownError": "알 수 없는 에러", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 3535fb15bc..daad7fd6e6 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Wybierz spośród następujących alternatywnych dostawców, aby wycofać (inaczej „bridge”) swój {asset} do {chain} .", "depositWith": "Wpłać za pomocą", "withdrawWith": "Wycofaj się z" + }, + "assetSelectScreen": { + "titleDeposit": "Wybierz zasób do zdeponowania", + "titleWithdraw": "Wybierz zasób do wypłaty", + "searchAssets": "Wyszukaj zasoby" + }, + "bridgeNetworkSelect": { + "title": "Wybierz sieć", + "searchPlaceholder": "Wyszukaj obsługiwane sieci" } }, "unknownError": "Nieznany błąd", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 94b53749e8..72272521f8 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Escolha um dos seguintes provedores alternativos para retirar (também conhecido como “ponte”) seu {asset} para {chain} .", "depositWith": "Deposite com", "withdrawWith": "Retirar com" + }, + "assetSelectScreen": { + "titleDeposit": "Selecione um ativo para depositar", + "titleWithdraw": "Selecione um ativo para retirar", + "searchAssets": "Pesquisar ativos" + }, + "bridgeNetworkSelect": { + "title": "Selecione a rede", + "searchPlaceholder": "Pesquisar redes suportadas" } }, "unknownError": "Erro desconhecido", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index e82829f9e2..282fb29690 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Alegeți dintre următorii furnizori alternativi pentru a vă retrage {asset} la {chain} .", "depositWith": "Depozit cu", "withdrawWith": "Retrage cu" + }, + "assetSelectScreen": { + "titleDeposit": "Selectați un activ de depus", + "titleWithdraw": "Selectați un activ de retras", + "searchAssets": "Căutați active" + }, + "bridgeNetworkSelect": { + "title": "Selecteaza reteaua", + "searchPlaceholder": "Căutați rețele acceptate" } }, "unknownError": "Eroare necunoscuta", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 9a0f811961..8940ed6a5d 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "Выберите одного из следующих альтернативных поставщиков для вывода (так называемого «моста») вашего {asset} в {chain} .", "depositWith": "Депозит с", "withdrawWith": "Вывод средств с помощью" + }, + "assetSelectScreen": { + "titleDeposit": "Выберите актив для депозита", + "titleWithdraw": "Выберите актив для вывода", + "searchAssets": "Поиск активов" + }, + "bridgeNetworkSelect": { + "title": "Выберите сеть", + "searchPlaceholder": "Поиск поддерживаемых сетей" } }, "unknownError": "Неизвестная ошибка", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 634eb89014..2011520592 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "{asset} {chain} .", "depositWith": "Şununla para yatır:", "withdrawWith": "Şununla çekil:" + }, + "assetSelectScreen": { + "titleDeposit": "Yatırılacak bir varlık seçin", + "titleWithdraw": "Çekilecek bir varlık seçin", + "searchAssets": "Varlıkları arayın" + }, + "bridgeNetworkSelect": { + "title": "Ağ seçin", + "searchPlaceholder": "Desteklenen ağları arayın" } }, "unknownError": "Bilinmeyen hata", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 7152ac61df..d0ebb0dfa0 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "从以下备选提供商中进行选择,将您的{asset}提取(又称为“桥接”)到{chain} 。", "depositWith": "存款", "withdrawWith": "提款" + }, + "assetSelectScreen": { + "titleDeposit": "选择要存入的资产", + "titleWithdraw": "选择要提取的资产", + "searchAssets": "搜索资产" + }, + "bridgeNetworkSelect": { + "title": "选择网络", + "searchPlaceholder": "搜索支持的网络" } }, "unknownError": "未知错误", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 4470b69710..777b2dfbc7 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "從以下替代提供者中進行選擇,將您的{asset}撤回(也稱為「bridge」)到{chain} 。", "depositWith": "存款於", "withdrawWith": "撤回與" + }, + "assetSelectScreen": { + "titleDeposit": "選擇要存入的資產", + "titleWithdraw": "選擇要提取的資產", + "searchAssets": "搜尋資產" + }, + "bridgeNetworkSelect": { + "title": "選擇網路", + "searchPlaceholder": "搜尋支援的網絡" } }, "unknownError": "未知錯誤", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index a2c4575340..8ae1bb96d0 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -929,6 +929,15 @@ "descriptionWithdraw": "從以下替代提供者中進行選擇,將您的{asset}撤回(也稱為「bridge」)到{chain} 。", "depositWith": "存款於", "withdrawWith": "撤回與" + }, + "assetSelectScreen": { + "titleDeposit": "選擇要存入的資產", + "titleWithdraw": "選擇要提取的資產", + "searchAssets": "搜尋資產" + }, + "bridgeNetworkSelect": { + "title": "選擇網路", + "searchPlaceholder": "搜尋支援的網絡" } }, "unknownError": "未知錯誤", From 2b8924c91faad16d6e4142b995de579616e02bde Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 20 Jun 2024 19:04:25 -0400 Subject: [PATCH 07/33] fix: more bridge options --- .../components/bridge/immersive/amount-screen.tsx | 1 + .../bridge/immersive/more-bridge-options.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index e6f26cf916..e7ac19a6fe 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -399,6 +399,7 @@ export const AmountScreen = observer( setIsMoreOptionsVisible(false)} /> diff --git a/packages/web/components/bridge/immersive/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx index e58640d090..02c01f3419 100644 --- a/packages/web/components/bridge/immersive/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -1,3 +1,4 @@ +import { MinimalAsset } from "@osmosis-labs/types"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import React from "react"; @@ -11,10 +12,11 @@ import { api } from "~/utils/trpc"; interface MoreBridgeOptionsProps extends ModalBaseProps { type: "deposit" | "withdraw"; + asset: MinimalAsset; } export const MoreBridgeOptions = observer( - ({ type, ...modalProps }: MoreBridgeOptionsProps) => { + ({ type, asset, ...modalProps }: MoreBridgeOptionsProps) => { const { accountStore, chainStore: { @@ -24,11 +26,7 @@ export const MoreBridgeOptions = observer( const wallet = accountStore.getWallet(accountStore.osmosisChainId); const { t } = useTranslation(); - // TODO: Use context state to get the fromAsset, toAsset, fromChain, and toChain - const { data: asset, isLoading: isLoadingAsset } = - api.edge.assets.getAssetWithPrice.useQuery({ - findMinDenomOrSymbol: "USDC", - }); + // TODO: Get fromChain, toChain, and address from props after supportTokens. const { data: externalUrlsData, isLoading: isLoadingExternalUrls } = api.bridgeTransfer.getExternalUrls.useQuery( { @@ -78,7 +76,7 @@ export const MoreBridgeOptions = observer( )}

- {isLoadingExternalUrls || isLoadingAsset ? ( + {isLoadingExternalUrls ? ( <> {new Array(3).fill(undefined).map((_, i) => ( From f6530012652bf5036e4e11bf905c41af1e26f45b Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 24 Jun 2024 14:36:57 -0400 Subject: [PATCH 08/33] improvement: @jonator feedback --- packages/types/src/asset-types.ts | 2 +- packages/types/src/bridge-types.ts | 1 + packages/types/src/index.ts | 1 + .../bridge/immersive/amount-screen.tsx | 2 +- .../immersive/bridge-network-select.tsx | 1 - .../bridge/immersive/immersive-bridge.tsx | 84 +++++++++---------- packages/web/hooks/bridge.tsx | 5 +- 7 files changed, 46 insertions(+), 50 deletions(-) create mode 100644 packages/types/src/bridge-types.ts diff --git a/packages/types/src/asset-types.ts b/packages/types/src/asset-types.ts index 51c0836f9c..c8225eb648 100644 --- a/packages/types/src/asset-types.ts +++ b/packages/types/src/asset-types.ts @@ -180,7 +180,7 @@ export interface Asset { variantGroupKey?: string; } -export type MinimalAsset = Currency & { +export type MinimalAsset = KeplrBaseCurrency & { coinGeckoId: string | undefined; coinName: string; isUnstable: boolean; diff --git a/packages/types/src/bridge-types.ts b/packages/types/src/bridge-types.ts new file mode 100644 index 0000000000..3a586b9bc4 --- /dev/null +++ b/packages/types/src/bridge-types.ts @@ -0,0 +1 @@ +export type BridgeTransactionDirection = "deposit" | "withdraw"; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 047060cfdb..018f2c0b9b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,5 +1,6 @@ export * from "./asset-types"; export * from "./autheticator-types"; +export * from "./bridge-types"; export * from "./chain-types"; export * from "./one-click-trading-types"; export * from "./staking-types"; diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index e7ac19a6fe..3e5a71b62e 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -23,7 +23,7 @@ import { api } from "~/utils/trpc"; interface AmountScreenProps { type: "deposit" | "withdraw"; - assetInOsmosis: MinimalAsset; + assetInOsmosis: MinimalAsset | undefined; } export const AmountScreen = observer( diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index f0ad0efb95..8601257bdd 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -44,7 +44,6 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { className="!max-w-[30rem]" {...modalProps} > - {/* TODO: Add translation */} { setQuery(nextValue); diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 9b18ff5839..1f3d0e227b 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -1,5 +1,5 @@ import { Transition } from "@headlessui/react"; -import { MinimalAsset } from "@osmosis-labs/types"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; import { memo, PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; @@ -26,7 +26,7 @@ enum ImmersiveBridgeScreens { Review = "2", } -const MemoizedChildren = memo(({ children }: PropsWithChildren<{}>) => { +const MemoizedChildren = memo(({ children }: PropsWithChildren) => { return <>{children}; }); @@ -38,12 +38,23 @@ export const ImmersiveBridgeFlow = ({ const [step, setStep] = useState( ImmersiveBridgeScreens.Asset ); - const [type, setType] = useState<"deposit" | "withdraw">("deposit"); + const [direction, setDirection] = + useState("deposit"); const { logEvent } = useAmplitudeAnalytics(); - const apiUtils = api.useUtils(); + const [selectedAssetDenom, setSelectedAssetDenom] = useState(); - const [assetInOsmosis, setAssetInOsmosis] = useState(); + const apiUtils = api.useUtils(); + const { data: assetInOsmosis } = api.edge.assets.getUserAsset.useQuery( + { + findMinDenomOrSymbol: selectedAssetDenom!, + }, + { + enabled: !isNil(selectedAssetDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; @@ -66,15 +77,19 @@ export const ImmersiveBridgeFlow = ({ setIsVisible(false); }; - const onOpen = (direction: "deposit" | "withdraw") => { + const onOpen = (direction: BridgeTransactionDirection) => { setIsVisible(true); - setType(direction); + setDirection(direction); }; return ( { + startBridge: ({ + direction, + }: { + direction: BridgeTransactionDirection; + }) => { onOpen(direction); }, bridgeAsset: async ({ @@ -82,41 +97,11 @@ export const ImmersiveBridgeFlow = ({ direction, }: { anyDenom: string; - direction: "deposit" | "withdraw"; + direction: BridgeTransactionDirection; }) => { onOpen(direction); setStep(ImmersiveBridgeScreens.Amount); - - const fetchAssetWithRetry = async (retries = 3) => { - for (let attempt = 1; attempt <= retries; attempt++) { - try { - const asset = await apiUtils.edge.assets.getUserAsset.fetch({ - findMinDenomOrSymbol: anyDenom, - }); - - if (!asset) { - console.error("Asset not found", anyDenom); - return undefined; - } - - return asset; - } catch (error) { - console.error(`Attempt ${attempt} failed:`, error); - if (attempt === retries) { - console.error("All attempts to fetch asset failed"); - return undefined; - } - } - } - }; - - const asset = await fetchAssetWithRetry(); - - if (!asset) { - return; - } - - setAssetInOsmosis(asset); + setSelectedAssetDenom(anyDenom); }, fiatRamp: ({ fiatRampKey, @@ -149,7 +134,7 @@ export const ImmersiveBridgeFlow = ({ leaveFrom="opacity-100" leaveTo="opacity-0" afterLeave={() => { - setAssetInOsmosis(undefined); + setSelectedAssetDenom(undefined); setStep(ImmersiveBridgeScreens.Asset); }} > @@ -198,10 +183,19 @@ export const ImmersiveBridgeFlow = ({ {({ setCurrentScreen }) => ( { setCurrentScreen(ImmersiveBridgeScreens.Amount); - setAssetInOsmosis(asset); + + // Set data to avoid waiting for the root assets query to fetch the data + apiUtils.edge.assets.getUserAsset.setData( + { + findMinDenomOrSymbol: asset.coinDenom, + }, + asset + ); + + setSelectedAssetDenom(asset.coinDenom); }} /> )} @@ -209,8 +203,8 @@ export const ImmersiveBridgeFlow = ({ {() => ( )} diff --git a/packages/web/hooks/bridge.tsx b/packages/web/hooks/bridge.tsx index b6f6aaf947..bede8085c0 100644 --- a/packages/web/hooks/bridge.tsx +++ b/packages/web/hooks/bridge.tsx @@ -1,3 +1,4 @@ +import { BridgeTransactionDirection } from "@osmosis-labs/types"; import { PropsWithChildren, Provider } from "react"; import { ImmersiveBridgeFlow } from "~/components/bridge/immersive"; @@ -9,11 +10,11 @@ import { useFeatureFlags } from "./use-feature-flags"; export type BridgeContext = { /** Start bridging without knowing the asset to bridge yet. */ - startBridge: (params: { direction: "deposit" | "withdraw" }) => void; + startBridge: (params: { direction: BridgeTransactionDirection }) => void; /** Start bridging a specified asset of coinMinimalDenom or symbol/denom. */ bridgeAsset: (params: { anyDenom: string; - direction: "deposit" | "withdraw"; + direction: BridgeTransactionDirection; }) => void; /** Open a specified fiat on ramp given a specific fiat ramp key and asset key. */ fiatRamp: (params: { fiatRampKey: FiatRampKey; assetKey: string }) => void; From 9135ad157a5ead799868e0cc201299f8ecebd7d2 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 24 Jun 2024 16:41:34 -0400 Subject: [PATCH 09/33] improvement: @jonator feedback --- .../complex/assets/__tests__/assets.spec.ts | 133 +++++++++++++++++- .../src/queries/complex/assets/index.ts | 32 +++++ packages/trpc/src/assets.ts | 10 ++ .../bridge/immersive/amount-screen.tsx | 38 ++--- .../bridge/immersive/asset-select-screen.tsx | 12 +- .../bridge/immersive/immersive-bridge.tsx | 33 ++--- .../immersive/use-bridge-supported-assets.ts | 1 + 7 files changed, 215 insertions(+), 44 deletions(-) create mode 100644 packages/web/components/bridge/immersive/use-bridge-supported-assets.ts diff --git a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts index 34335288b0..d0141fd9df 100644 --- a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts +++ b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts @@ -1,5 +1,5 @@ import { AssetLists as MockAssetLists } from "../../../__tests__/mock-asset-lists"; -import { getAsset, getAssets } from "../index"; +import { getAsset, getAssets, getAssetWithVariants } from "../index"; describe("getAssets", () => { describe("search", () => { @@ -85,3 +85,134 @@ describe("getAsset", () => { ).toThrow(); }); }); + +describe("getAssetWithVariants", () => { + it("should return the asset with its variants, with the canonical asset first", () => { + const result = getAssetWithVariants({ + assetLists: MockAssetLists, + anyDenom: "USDC", + }); + + expect(result).toMatchInlineSnapshot(` + [ + { + "coinDecimals": 6, + "coinDenom": "USDC", + "coinGeckoId": "usd-coin", + "coinImageUrl": "/tokens/generated/usdc.svg", + "coinMinimalDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "coinName": "USDC", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "uusdc", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "USDC.axl", + "coinGeckoId": "axlusdc", + "coinImageUrl": "/tokens/generated/usdc.axl.svg", + "coinMinimalDenom": "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", + "coinName": "USD Coin (Axelar)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "uusdc", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "polygon.USDC.axl", + "coinGeckoId": "usd-coin", + "coinImageUrl": "/tokens/generated/polygon.usdc.axl.svg", + "coinMinimalDenom": "ibc/231FD77ECCB2DB916D314019DA30FE013202833386B1908A191D16989AD80B5A", + "coinName": "USD Coin (Polygon)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "polygon-uusdc", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "avalanche.USDC.axl", + "coinGeckoId": "usd-coin", + "coinImageUrl": "/tokens/generated/avalanche.usdc.axl.svg", + "coinMinimalDenom": "ibc/F17C9CA112815613C5B6771047A093054F837C3020CBA59DFFD9D780A8B2984C", + "coinName": "USD Coin (Avalanche)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "avalanche-uusdc", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "USDC.grv", + "coinGeckoId": "gravity-bridge-usdc", + "coinImageUrl": "/tokens/generated/usdc.grv.svg", + "coinMinimalDenom": "ibc/9F9B07EF9AD291167CF5700628145DE1DEB777C2CFC7907553B24446515F6D0E", + "coinName": "USDC (Gravity Bridge)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "gravity0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "USDC.wh", + "coinGeckoId": undefined, + "coinImageUrl": "/tokens/generated/usdc.wh.svg", + "coinMinimalDenom": "ibc/6B99DB46AA9FF47162148C1726866919E44A6A5E0274B90912FD17E19A337695", + "coinName": "USD Coin (Wormhole)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt", + "variantGroupKey": "USDC", + }, + { + "coinDecimals": 6, + "coinDenom": "solana.USDC.wh", + "coinGeckoId": undefined, + "coinImageUrl": "/tokens/generated/solana.usdc.wh.svg", + "coinMinimalDenom": "ibc/F08DE332018E8070CC4C68FE06E04E254F527556A614F5F8F9A68AF38D367E45", + "coinName": "Solana USD Coin (Wormhole)", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/HJk1XMDRNUbRrpKkNZYui7SwWDMjXZAsySzqgyNcQoU3", + "variantGroupKey": "USDC", + }, + ] + `); + }); + + it("should throw an error if the asset is not found", () => { + expect(() => + getAssetWithVariants({ + assetLists: MockAssetLists, + anyDenom: "unotexist", + }) + ).toThrow("unotexist not found in asset list"); + }); + + it("Should still return assets even if they have no variants", () => { + expect( + getAssetWithVariants({ + assetLists: MockAssetLists, + anyDenom: "ATOM", + }) + ).toMatchInlineSnapshot(` + [ + { + "coinDecimals": 6, + "coinDenom": "ATOM", + "coinGeckoId": "cosmos", + "coinImageUrl": "/tokens/generated/atom.svg", + "coinMinimalDenom": "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", + "coinName": "Cosmos Hub", + "isUnstable": false, + "isVerified": true, + "sourceDenom": "uatom", + "variantGroupKey": "ATOM", + }, + ] + `); + }); +}); diff --git a/packages/server/src/queries/complex/assets/index.ts b/packages/server/src/queries/complex/assets/index.ts index dc619e92ed..e14b037445 100644 --- a/packages/server/src/queries/complex/assets/index.ts +++ b/packages/server/src/queries/complex/assets/index.ts @@ -39,6 +39,38 @@ export function getAsset({ return asset; } +/** + * Retrieves an asset along with its variants from the provided asset lists. + * Canonical asset is placed at the beginning of the array. + * + * @throws Will throw an error if the asset is not found in the asset lists. + */ +export function getAssetWithVariants({ + assetLists, + anyDenom, +}: { + assetLists: AssetList[]; + anyDenom: string; +}) { + const asset = getAsset({ + assetLists, + anyDenom, + }); + + if (!asset) throw new Error(anyDenom + " not found in asset list"); + + const variants = getAssets({ + assetLists, + includePreview: true, + }).filter((a) => a?.variantGroupKey === asset.variantGroupKey); + + return ( + variants + // place the canonical asset at the beginning + .sort((a) => (a.coinDenom === asset.variantGroupKey ? -1 : 1)) + ); +} + /** Returns minimal asset information for assets in asset list. Return values can double as the `Currency` type. * Search was added to this function since members of the asset list type are search before mapped * into minimal assets. See `searchableAssetListAssetKeys` for the keys that are searched. diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index 8768bee846..7eeca4a4ca 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -14,6 +14,7 @@ import { getAssetPrice, getAssets, getAssetWithUserBalance, + getAssetWithVariants, getBridgeAsset, getCoinGeckoCoinMarketChart, getMarketAsset, @@ -105,6 +106,15 @@ export const assetsRouter = createTRPCRouter({ limit, }) ), + getCanonicalAssetWithVariants: publicProcedure + .input( + z.object({ + findMinDenomOrSymbol: z.string(), + }) + ) + .query(async ({ input: { findMinDenomOrSymbol }, ctx }) => + getAssetWithVariants({ ...ctx, anyDenom: findMinDenomOrSymbol }) + ), getAssetPrice: publicProcedure .input( z.object({ diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 3e5a71b62e..f30d513896 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -23,11 +23,15 @@ import { api } from "~/utils/trpc"; interface AmountScreenProps { type: "deposit" | "withdraw"; - assetInOsmosis: MinimalAsset | undefined; + + /** + * Includes both the canonical asset and its variants. + */ + assetsInOsmosis: MinimalAsset[] | undefined; } export const AmountScreen = observer( - ({ type, assetInOsmosis }: AmountScreenProps) => { + ({ type, assetsInOsmosis }: AmountScreenProps) => { const { accountStore } = useStore(); const wallet = accountStore.getWallet(accountStore.osmosisChainId); const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); @@ -48,16 +52,17 @@ export const AmountScreen = observer( findChainNameOrId: "noble", }); - const { price: assetInOsmosisPrice, isLoading } = usePrice(assetInOsmosis); + const canonicalAsset = assetsInOsmosis?.[0]; + const { price: assetInOsmosisPrice, isLoading } = usePrice(canonicalAsset); const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); - // TODO: Add skeleton loaders if ( isLoading || - !assetInOsmosis || + !assetsInOsmosis || + !canonicalAsset || !assetInOsmosisPrice || !osmosisChain || !nobleChain @@ -66,11 +71,11 @@ export const AmountScreen = observer( } const cryptoAmountPretty = new CoinPretty( - assetInOsmosis, + canonicalAsset, cryptoAmount === "" ? new Dec(0) : new Dec(cryptoAmount).mul( - DecUtils.getTenExponentN(assetInOsmosis.coinDecimals) + DecUtils.getTenExponentN(canonicalAsset.coinDecimals) ) ); @@ -131,10 +136,10 @@ export const AmountScreen = observer( token image{" "} - {assetInOsmosis.coinDenom} + {canonicalAsset.coinDenom}
@@ -323,14 +328,14 @@ export const AmountScreen = observer( @@ -399,7 +404,8 @@ export const AmountScreen = observer( setIsMoreOptionsVisible(false)} /> diff --git a/packages/web/components/bridge/immersive/asset-select-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx index 26b6aabca5..a5e49a7979 100644 --- a/packages/web/components/bridge/immersive/asset-select-screen.tsx +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useState } from "react"; import { Icon } from "~/components/assets"; import { SearchBox } from "~/components/input"; import { Intersection } from "~/components/intersection"; -import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { MainnetAssetSymbols, @@ -22,7 +22,7 @@ import { ActivateUnverifiedTokenConfirmation } from "~/modals/activate-unverifie import { useStore } from "~/stores"; import { UnverifiedAssetsState } from "~/stores/user-settings/unverified-assets"; import { formatPretty } from "~/utils/formatter"; -import { api } from "~/utils/trpc"; +import { api, RouterOutputs } from "~/utils/trpc"; const variantsNotToBeExcluded = ["WBTC", "BTC"] satisfies ( | MainnetVariantGroupKeys @@ -41,7 +41,9 @@ const prioritizedDenoms = [ interface AssetSelectScreenProps { type: "deposit" | "withdraw"; - onSelectAsset: (asset: MinimalAsset) => void; + onSelectAsset: ( + asset: RouterOutputs["edge"]["assets"]["getImmersiveBridgeAssets"]["items"][number] + ) => void; } export const AssetSelectScreen = observer( @@ -136,9 +138,7 @@ export const AssetSelectScreen = observer(
{isLoading ? ( <> - {new Array(7).fill(undefined).map((_, i) => ( - - ))} + ) : ( <> diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 1f3d0e227b..8c6a6144c3 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -44,17 +44,17 @@ export const ImmersiveBridgeFlow = ({ const [selectedAssetDenom, setSelectedAssetDenom] = useState(); - const apiUtils = api.useUtils(); - const { data: assetInOsmosis } = api.edge.assets.getUserAsset.useQuery( - { - findMinDenomOrSymbol: selectedAssetDenom!, - }, - { - enabled: !isNil(selectedAssetDenom), - cacheTime: 10 * 60 * 1000, // 10 minutes - staleTime: 10 * 60 * 1000, // 10 minutes - } - ); + const { data: canonicalAssetsWithVariants } = + api.edge.assets.getCanonicalAssetWithVariants.useQuery( + { + findMinDenomOrSymbol: selectedAssetDenom!, + }, + { + enabled: !isNil(selectedAssetDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; @@ -186,15 +186,6 @@ export const ImmersiveBridgeFlow = ({ type={direction} onSelectAsset={(asset) => { setCurrentScreen(ImmersiveBridgeScreens.Amount); - - // Set data to avoid waiting for the root assets query to fetch the data - apiUtils.edge.assets.getUserAsset.setData( - { - findMinDenomOrSymbol: asset.coinDenom, - }, - asset - ); - setSelectedAssetDenom(asset.coinDenom); }} /> @@ -204,7 +195,7 @@ export const ImmersiveBridgeFlow = ({ {() => ( )} diff --git a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts new file mode 100644 index 0000000000..091d92929d --- /dev/null +++ b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts @@ -0,0 +1 @@ +export const useBridgeSupportedAssets = () => {}; From 566e72a1486954dccebca11e74f2a274a908332f Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 24 Jun 2024 19:54:37 -0400 Subject: [PATCH 10/33] feat: aggregate chains and assets --- packages/bridge/src/index.ts | 1 + packages/bridge/src/interface.ts | 2 +- packages/trpc/src/chains.ts | 26 +- packages/types/src/chain-types.ts | 10 + .../bridge/immersive/amount-screen.tsx | 266 ++++++++++++------ .../immersive/bridge-network-select.tsx | 97 +++---- .../bridge/immersive/immersive-bridge.tsx | 1 + .../immersive/use-bridge-supported-assets.ts | 1 - .../immersive/use-bridges-supported-assets.ts | 86 ++++++ packages/web/localizations/en.json | 1 + .../web/server/api/routers/bridge-transfer.ts | 92 ++++++ 11 files changed, 407 insertions(+), 176 deletions(-) delete mode 100644 packages/web/components/bridge/immersive/use-bridge-supported-assets.ts create mode 100644 packages/web/components/bridge/immersive/use-bridges-supported-assets.ts diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index 44e9c34053..5355c23b94 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -2,6 +2,7 @@ export * from "./axelar"; export * from "./bridge-providers"; export * from "./chain"; export * from "./errors"; +export * from "./ethereum"; export * from "./interface"; export * from "./skip"; export * from "./squid"; diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index 96521c586e..3f9345dcb0 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -176,7 +176,7 @@ const bridgeAssetSchema = z.object({ export type BridgeAsset = z.infer; -const bridgeSupportedAssetsSchema = z.object({ +export const bridgeSupportedAssetsSchema = z.object({ /** * The originating chain information. */ diff --git a/packages/trpc/src/chains.ts b/packages/trpc/src/chains.ts index 01905780d3..7392e9aaf7 100644 --- a/packages/trpc/src/chains.ts +++ b/packages/trpc/src/chains.ts @@ -1,8 +1,4 @@ -import { - CursorPaginationSchema, - getChain, - maybeCachePaginatedItems, -} from "@osmosis-labs/server"; +import { getChain } from "@osmosis-labs/server"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; @@ -20,24 +16,4 @@ export const chainsRouter = createTRPCRouter({ chainNameOrId: findChainNameOrId, }) ), - getChains: publicProcedure - .input( - CursorPaginationSchema.merge(z.object({ search: z.string().optional() })) - ) - .query(async ({ input: { cursor, limit, search }, ctx }) => - maybeCachePaginatedItems({ - cacheKey: "chains", - getFreshItems: () => - Promise.resolve( - ctx.chainList.filter((chain) => { - return search - ? chain.chain_name.includes(search) || - chain.pretty_name.includes(search) - : true; - }) - ), - cursor, - limit, - }) - ), }); diff --git a/packages/types/src/chain-types.ts b/packages/types/src/chain-types.ts index 067e3061e7..c4dafdd40d 100644 --- a/packages/types/src/chain-types.ts +++ b/packages/types/src/chain-types.ts @@ -18,6 +18,16 @@ export interface Chain { bech32_config: Bech32Config; slip44: number; alternative_slip44s?: number[]; + logoURIs?: { + png?: string; + svg?: string; + layout?: "logomark"; + theme?: { + primary_color_hex?: string; + dark_mode?: false; + circle?: true; + }; + }; fees: { fee_tokens: FeeToken[]; }; diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index f30d513896..6427853591 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,16 +1,18 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { MinimalAsset } from "@osmosis-labs/types"; -import { isNumeric, noop } from "@osmosis-labs/utils"; +import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { FunctionComponent, ReactNode, useState } from "react"; +import { FunctionComponent, ReactNode, useEffect, useState } from "react"; import { Icon } from "~/components/assets"; import { BridgeNetworkSelect } from "~/components/bridge/immersive/bridge-network-select"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; +import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; @@ -23,6 +25,7 @@ import { api } from "~/utils/trpc"; interface AmountScreenProps { type: "deposit" | "withdraw"; + selectedDenom: string; /** * Includes both the canonical asset and its variants. @@ -31,7 +34,7 @@ interface AmountScreenProps { } export const AmountScreen = observer( - ({ type, assetsInOsmosis }: AmountScreenProps) => { + ({ type, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { const { accountStore } = useStore(); const wallet = accountStore.getWallet(accountStore.osmosisChainId); const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); @@ -52,15 +55,38 @@ export const AmountScreen = observer( findChainNameOrId: "noble", }); + const [receiveAsset, setReceiveAsset] = useState(); + const canonicalAsset = assetsInOsmosis?.[0]; - const { price: assetInOsmosisPrice, isLoading } = usePrice(canonicalAsset); + const { + price: assetInOsmosisPrice, + isLoading: isLoadingCanonicalAssetPrice, + } = usePrice(receiveAsset); const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); + const { supportedAssets, supportedChains } = useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); + + useEffect(() => { + if (!isNil(assetsInOsmosis) && setReceiveAsset) { + // TODO: Get canonical asset from supported assets + setReceiveAsset( + assetsInOsmosis.find((asset) => asset.coinDenom === selectedDenom)! + ); + } + }, [assetsInOsmosis, selectedDenom]); + if ( - isLoading || + isLoadingCanonicalAssetPrice || + isNil(supportedAssets) || !assetsInOsmosis || !canonicalAsset || !assetInOsmosisPrice || @@ -125,6 +151,12 @@ export const AmountScreen = observer( type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); }; + const dropdownActiveItemIcon = ( +
+ +
+ ); + return (
@@ -154,7 +186,11 @@ export const AmountScreen = observer(
- + {}} + > {nobleChain.pretty_name} @@ -282,92 +318,129 @@ export const AmountScreen = observer(
- - {({ open }) => ( -
- -
-
- - {t("transfer.receiveAsset")} - - -

- {t("transfer.receiveAsset")} -

-

- {t("transfer.receiveAssetDescription")} -

-
- } - > - - -
- -
- USDC - 1 && ( + + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

+ {t("transfer.receiveAsset")} +

+

+ {t("transfer.receiveAssetDescription")} +

+
} - )} - /> -
-
- - - - - - - - - - -
- )} -
+
+ + + + {assetsInOsmosis.map((asset, index) => { + const onClick = () => { + setReceiveAsset(asset); + }; + + const isConvert = + asset.coinDenom === asset.variantGroupKey; + const isSelected = + receiveAsset?.coinDenom === asset.coinDenom; + + // Is canonical asset + if (index === 0) { + return ( + + + + ); + } + + return ( + + + + ); + })} + +
+ )} + + )}
@@ -421,7 +494,9 @@ const ChainSelectorButton: FunctionComponent<{ readonly?: boolean; children: ReactNode; chainLogo: string; -}> = ({ readonly, children, chainLogo: _chainLogo }) => { + chains?: ReturnType["supportedChains"]; + onSelectChain?: (chain: BridgeChain) => void; +}> = ({ readonly, children, chainLogo: _chainLogo, chains, onSelectChain }) => { const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); if (readonly) { @@ -448,10 +523,17 @@ const ChainSelectorButton: FunctionComponent<{ height={12} /> - setIsNetworkSelectVisible(false)} - /> + {!isNil(chains) && !isNil(onSelectChain) && ( + { + onSelectChain(chain); + setIsNetworkSelectVisible(false); + }} + onRequestClose={() => setIsNetworkSelectVisible(false)} + /> + )} ); }; diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index 8601257bdd..ba3c4c1ebd 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -1,42 +1,34 @@ +import type { BridgeChain } from "@osmosis-labs/bridge"; import { debounce } from "debounce"; import React, { useMemo, useState } from "react"; import { SearchBox } from "~/components/input"; -import { Intersection } from "~/components/intersection"; -import { SkeletonLoader, Spinner } from "~/components/loaders"; import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; -import { api } from "~/utils/trpc"; -export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { +interface BridgeNetworkSelectProps extends ModalBaseProps { + chains: { + prettyName: string; + chainId: BridgeChain["chainId"]; + chainType: BridgeChain["chainType"]; + }[]; + onSelectChain: (chain: BridgeChain) => void; +} + +export const BridgeNetworkSelect = ({ + chains, + onSelectChain, + ...modalProps +}: BridgeNetworkSelectProps) => { const { t } = useTranslation(); const [query, setQuery] = useState(""); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - api.edge.chains.getChains.useInfiniteQuery( - { - limit: 50, - search: query, - }, - { - enabled: modalProps.isOpen, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialCursor: 0, - keepPreviousData: true, - trpc: { - context: { - skipBatch: true, - }, - }, - } + const filteredChains = useMemo(() => { + return chains.filter(({ prettyName }) => + prettyName.toLowerCase().includes(query.toLowerCase()) ); - - const chains = useMemo( - () => data?.pages.flatMap((page) => page?.items) ?? [], - [data] - ); - const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; + }, [chains, query]); return ( { size="full" />
- {isLoading ? ( - <> - {new Array(3).fill(undefined).map((_, i) => ( - - ))} - - ) : ( - <> - {chains.map(({ chain_name, pretty_name }) => ( - - ))} - { - if (canLoadMore) { - fetchNextPage(); - } - }} - /> - {isFetchingNextPage && ( -
- -
- )} - - )} + {filteredChains.map((chain) => ( + + ))}
); diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 8c6a6144c3..318fb5bee2 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -196,6 +196,7 @@ export const ImmersiveBridgeFlow = ({ )} diff --git a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts deleted file mode 100644 index 091d92929d..0000000000 --- a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts +++ /dev/null @@ -1 +0,0 @@ -export const useBridgeSupportedAssets = () => {}; diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts new file mode 100644 index 0000000000..6a46193bf9 --- /dev/null +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -0,0 +1,86 @@ +import type { Bridge, BridgeChain } from "@osmosis-labs/bridge"; +import { MinimalAsset } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import { useMemo } from "react"; + +import { api } from "~/utils/trpc"; + +const bridgeKeys: Bridge[] = ["Skip", "Squid", "Axelar", "IBC"]; + +export const useBridgesSupportedAssets = ({ + assets, + chain, +}: { + assets: MinimalAsset[] | undefined; + chain: BridgeChain; +}) => { + const supportedAssetsResults = api.useQueries((t) => + bridgeKeys.flatMap((bridge) => + (assets ?? []).map((asset) => + t.bridgeTransfer.getSupportedAssetsByBridge( + { + bridge, + asset: { + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + denom: asset.coinDenom, + sourceDenom: asset.sourceDenom, + }, + chain, + }, + { + enabled: !isNil(assets), + staleTime: 30_000, + cacheTime: 30_000, + // Disable retries, as useQueries + // will block successful queries from being returned + // if failed queries are being returned + // until retry starts returning false. + // This causes slow UX even though there's a + // query that the user can use. + retry: false, + + // prevent batching so that fast routers can + // return requests faster than the slowest router + trpc: { + context: { + skipBatch: true, + }, + }, + } + ) + ) + ) + ); + + const successfulQueries = useMemo( + () => + supportedAssetsResults.filter( + (data): data is NonNullable> => + !isNil(data) && data?.isSuccess + ), + [supportedAssetsResults] + ); + + const supportedAssets = useMemo(() => { + return successfulQueries.flatMap( + ({ data }) => data?.supportedAssets.assets ?? [] + ); + }, [successfulQueries]); + + const supportedChains = useMemo(() => { + return Array.from( + // Remove duplicate chains + new Map( + successfulQueries + .flatMap(({ data }) => data!.supportedAssets.availableChains) + .map(({ chainId, chainType, prettyName }) => [ + chainId, + { chainId, chainType, prettyName }, + ]) + ).values() + ); + }, [successfulQueries]); + + return { supportedAssets, supportedChains }; +}; diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 08e903071d..d401a5fd3b 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -921,6 +921,7 @@ "moreDepositOptions": "More deposit options", "moreWithdrawOptions": "More withdraw options", "convertTo": "Convert to", + "depositAs": "Deposit as", "recommended": "Recommended", "moreBridgeOptions": { "titleDeposit": "More deposit options", diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 452c585da7..d25d345e0c 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -3,12 +3,15 @@ import { Bridge, BridgeCoin, BridgeProviders, + bridgeSupportedAssetsSchema, + EthereumChainInfo, getBridgeExternalUrlSchema, getBridgeQuoteSchema, } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY, getAssetPrice, + getChain, getTimeoutHeight, } from "@osmosis-labs/server"; import { createTRPCRouter, publicProcedure } from "@osmosis-labs/trpc"; @@ -188,6 +191,95 @@ export const bridgeTransferRouter = createTRPCRouter({ }; }), + getSupportedAssetsByBridge: publicProcedure + .input(bridgeSupportedAssetsSchema.extend({ bridge: z.string() })) + .query(async ({ input, ctx }) => { + const bridgeProviders = new BridgeProviders( + process.env.NEXT_PUBLIC_SQUID_INTEGRATOR_ID!, + { + ...ctx, + env: IS_TESTNET ? "testnet" : "mainnet", + cache: lruCache, + getTimeoutHeight: ({ destinationAddress }) => + getTimeoutHeight({ ...ctx, destinationAddress }), + } + ); + + const bridgeProvider = + bridgeProviders.bridges[ + input.bridge as keyof typeof bridgeProviders.bridges + ]; + + if (!bridgeProvider) { + throw new Error("Invalid bridge provider id: " + input.bridge); + } + + const supportedAssetFn = () => bridgeProvider.getSupportedAssets(input); + + /** If the bridge takes longer than 10 seconds to respond, we should timeout that quote. */ + const supportedAssets = await timeout(supportedAssetFn, 10 * 1000)(); + + const eventualChains = Array.from( + // Remove duplicate chains + new Map( + supportedAssets.map(({ chainId, chainType }) => [ + chainId, + { chainId, chainType }, + ]) + ).values() + ); + + const availableChains = eventualChains + .map(({ chainId, chainType }) => { + if (chainType === "evm") { + // TODO: Find a way to get eth chains from `getChain` function + const evmChain = Object.values(EthereumChainInfo).find( + (chain) => chain.id === chainId + ); + + if (!evmChain) { + return undefined; + } + + return { + prettyName: evmChain.name, + chainId: evmChain.id, + chainType, + }; + } else if (chainType === "cosmos") { + const cosmosChain = getChain({ + chainList: ctx.chainList, + chainNameOrId: String(chainId), + }); + + if (!cosmosChain) { + return undefined; + } + + return { + prettyName: cosmosChain.chain_name, + chainId: cosmosChain.chain_id, + chainType, + }; + } + + return undefined; + }) + .filter((chain): chain is NonNullable => Boolean(chain)); + + return { + supportedAssets: { + provider: { + id: bridgeProvider.providerName as Bridge, + logoUrl: BridgeLogoUrls[bridgeProvider.providerName as Bridge], + }, + originalAsset: input.asset, + assets: supportedAssets, + availableChains, + }, + }; + }), + /** * Provide the transfer request for a given bridge transfer. */ From 1162d5978306845b6a05f8bf6ec5c8db00e302cd Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 24 Jun 2024 23:06:17 -0400 Subject: [PATCH 11/33] feat: merge assets by chain id --- .../immersive/use-bridges-supported-assets.ts | 29 ++++++++++++++++--- .../web/server/api/routers/bridge-transfer.ts | 16 ++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts index 6a46193bf9..6e1eab8ffb 100644 --- a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -3,7 +3,7 @@ import { MinimalAsset } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; import { useMemo } from "react"; -import { api } from "~/utils/trpc"; +import { api, RouterOutputs } from "~/utils/trpc"; const bridgeKeys: Bridge[] = ["Skip", "Squid", "Axelar", "IBC"]; @@ -63,9 +63,30 @@ export const useBridgesSupportedAssets = ({ ); const supportedAssets = useMemo(() => { - return successfulQueries.flatMap( - ({ data }) => data?.supportedAssets.assets ?? [] - ); + return successfulQueries.reduce((acc, { data }) => { + if (!data) return acc; + + // Merge all assets from providers by chain id + Object.entries(data.supportedAssets.assets).forEach(([key, value]) => { + if (acc[key]) { + acc[key] = acc[key].concat(value); + } else { + acc[key] = value; + } + }); + + // Remove duplicates + Object.keys(acc).forEach((key) => { + if (Array.isArray(acc[key])) { + acc[key] = acc[key].filter( + (value, index, self) => + index === self.findIndex((t) => t.address === value.address) + ); + } + }); + + return acc; + }, {} as RouterOutputs["bridgeTransfer"]["getSupportedAssetsByBridge"]["supportedAssets"]["assets"]); }, [successfulQueries]); const supportedChains = useMemo(() => { diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index d25d345e0c..397a2edc68 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -1,6 +1,7 @@ import { Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { Bridge, + BridgeChain, BridgeCoin, BridgeProviders, bridgeSupportedAssetsSchema, @@ -219,6 +220,18 @@ export const bridgeTransferRouter = createTRPCRouter({ /** If the bridge takes longer than 10 seconds to respond, we should timeout that quote. */ const supportedAssets = await timeout(supportedAssetFn, 10 * 1000)(); + const assetsByChainId = supportedAssets.reduce< + Record + >((acc, asset) => { + if (!acc[asset.chainId]) { + acc[asset.chainId] = []; + } + + acc[asset.chainId].push(asset); + + return acc; + }, {}); + const eventualChains = Array.from( // Remove duplicate chains new Map( @@ -273,8 +286,7 @@ export const bridgeTransferRouter = createTRPCRouter({ id: bridgeProvider.providerName as Bridge, logoUrl: BridgeLogoUrls[bridgeProvider.providerName as Bridge], }, - originalAsset: input.asset, - assets: supportedAssets, + assets: assetsByChainId, availableChains, }, }; From 84e92fa7ccf0d1b8c3cd9e1e4339048ff46a6490 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Tue, 25 Jun 2024 17:48:20 -0400 Subject: [PATCH 12/33] feat: aggregate assets display them initially on amount screen --- packages/bridge/src/axelar/tokens.ts | 4 +- packages/bridge/src/chain.ts | 22 --- packages/bridge/src/skip/index.ts | 8 +- packages/bridge/src/squid/index.ts | 9 +- packages/server/package.json | 3 +- .../src/queries/complex/assets/ethereum.ts | 9 + .../src/queries/complex/assets/index.ts | 2 +- packages/trpc/src/assets.ts | 2 +- packages/trpc/src/local-assets.ts | 41 +++++ packages/utils/package.json | 3 +- packages/{bridge => utils}/src/ethereum.ts | 45 +++-- packages/utils/src/index.ts | 1 + .../bridge/immersive/amount-screen.tsx | 164 +++++++++++------- .../bridge/immersive/asset-select-screen.tsx | 11 +- .../immersive/bridge-network-select.tsx | 19 +- .../bridge/immersive/immersive-bridge.tsx | 2 +- .../immersive/use-bridges-supported-assets.ts | 130 +++++++++++--- packages/web/config/generate-lists.ts | 8 +- packages/web/integrations/ethereum/types.ts | 7 - packages/web/modals/bridge-transfer-v2.tsx | 14 +- .../web/server/api/routers/bridge-transfer.ts | 28 +-- 21 files changed, 364 insertions(+), 168 deletions(-) delete mode 100644 packages/bridge/src/chain.ts create mode 100644 packages/server/src/queries/complex/assets/ethereum.ts create mode 100644 packages/trpc/src/local-assets.ts rename packages/{bridge => utils}/src/ethereum.ts (81%) diff --git a/packages/bridge/src/axelar/tokens.ts b/packages/bridge/src/axelar/tokens.ts index 099ebbf76c..e5231b731a 100644 --- a/packages/bridge/src/axelar/tokens.ts +++ b/packages/bridge/src/axelar/tokens.ts @@ -1,5 +1,5 @@ -import { SourceChain } from "../chain"; -import { EthereumChainInfo } from "../ethereum"; +import { EthereumChainInfo, SourceChain } from "@osmosis-labs/utils"; + import { BridgeEnvironment } from "../interface"; export type SourceChainTokenConfig = { diff --git a/packages/bridge/src/chain.ts b/packages/bridge/src/chain.ts deleted file mode 100644 index 8a2d9d7ba2..0000000000 --- a/packages/bridge/src/chain.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** Human-displayable global source chain identifiers. - * TODO: use global chain IDs instead of display names as keys - */ -export type SourceChain = - | "Bitcoin" - | "Bitcoin Testnet" - | "Aurora Testnet" - | "Avalanche" - | "Avalanche Fuji Testnet" - | "Binance Smart Chain" - | "BSC Testnet" - | "Ethereum" - | "Goerli Testnet" - | "Fantom" - | "Fantom Testnet" - | "Moonbeam" - | "Moonbase Alpha" - | "Polygon" - | "Mumbai" - | "Filecoin" - | "Filecoin Hyperspace" - | "Arbitrum"; diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index af8103cc20..26997122a7 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -4,7 +4,11 @@ import { CoinPretty } from "@keplr-wallet/unit"; import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; -import { isNil } from "@osmosis-labs/utils"; +import { + EthereumChainInfo, + isNil, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import cachified from "cachified"; import { Address, @@ -19,7 +23,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, @@ -331,6 +334,7 @@ export class SkipBridgeProvider implements BridgeProvider { ...chainInfo, address: sharedOriginAsset.denom, denom: + sharedOriginAsset.recommended_symbol ?? sharedOriginAsset.symbol ?? sharedOriginAsset.name ?? sharedOriginAsset.denom, diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 058e4c1685..28956c47ff 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -8,7 +8,13 @@ import { } from "@0xsquid/sdk"; import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; -import { apiClient, ApiClientError, isNil } from "@osmosis-labs/utils"; +import { + apiClient, + ApiClientError, + EthereumChainInfo, + isNil, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import { cachified } from "cachified"; import Long from "long"; import { @@ -21,7 +27,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, diff --git a/packages/server/package.json b/packages/server/package.json index 049e2b499b..961bf76d4a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,8 @@ "jest-util": "^29.7.0", "lru-cache": "^10.0.1", "superjson": "^2.2.1", - "zod": "^3.22.4" + "zod": "^3.22.4", + "viem": "2.13.3" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/server/src/queries/complex/assets/ethereum.ts b/packages/server/src/queries/complex/assets/ethereum.ts new file mode 100644 index 0000000000..84c7cc6303 --- /dev/null +++ b/packages/server/src/queries/complex/assets/ethereum.ts @@ -0,0 +1,9 @@ +import { NativeEVMTokenConstantAddress } from "@osmosis-labs/utils"; + +export function getEvmBalance({ address, userAddress, chainId }: { + address: string; + userAddress: string; + chainId: number; +}) { + if(address === NativeEVMTokenConstantAddress) +} diff --git a/packages/server/src/queries/complex/assets/index.ts b/packages/server/src/queries/complex/assets/index.ts index e14b037445..114a440c67 100644 --- a/packages/server/src/queries/complex/assets/index.ts +++ b/packages/server/src/queries/complex/assets/index.ts @@ -67,7 +67,7 @@ export function getAssetWithVariants({ return ( variants // place the canonical asset at the beginning - .sort((a) => (a.coinDenom === asset.variantGroupKey ? -1 : 1)) + .sort((a) => (a.coinMinimalDenom === asset.variantGroupKey ? -1 : 1)) ); } diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index 7eeca4a4ca..d6a4215d48 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -571,7 +571,7 @@ export const assetsRouter = createTRPCRouter({ asset.variantGroupKey as (typeof variantsNotToBeExcluded)[number] ) ) { - return asset.variantGroupKey === asset.coinDenom; + return asset.variantGroupKey === asset.coinMinimalDenom; } return true; diff --git a/packages/trpc/src/local-assets.ts b/packages/trpc/src/local-assets.ts new file mode 100644 index 0000000000..e60d5d921e --- /dev/null +++ b/packages/trpc/src/local-assets.ts @@ -0,0 +1,41 @@ +import { getAsset, getAssetWithUserBalance } from "@osmosis-labs/server"; +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "./api"; +import { UserOsmoAddressSchema } from "./parameter-types"; + +const evmSchema = z.object({ + type: z.literal("evm"), + address: z.string().startsWith("0x"), + userAddress: z.string(), + chainId: z.number(), +}); + +const cosmosSchema = z + .object({ + type: z.literal("cosmos"), + anyDenom: z.string(), + }) + .merge(UserOsmoAddressSchema.required()); + +export const localAssetsRouter = createTRPCRouter({ + getBalance: publicProcedure + .input(z.union([evmSchema, cosmosSchema])) + .query(async ({ input, ctx }) => { + if (input.type === "evm") { + } + + if (input.type === "cosmos") { + const asset = getAsset({ + ...ctx, + anyDenom: input.anyDenom, + }); + + return await getAssetWithUserBalance({ + ...ctx, + asset, + userOsmoAddress: input.userOsmoAddress, + }); + } + }), +}); diff --git a/packages/utils/package.json b/packages/utils/package.json index 2aead4587f..f6d2905515 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -16,7 +16,8 @@ "dependencies": { "@keplr-wallet/unit": "0.10.24-ibc.go.v7.hot.fix", "@osmosis-labs/types": "^1.0.0", - "sha.js": "^2.4.11" + "sha.js": "^2.4.11", + "viem": "2.13.3" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/bridge/src/ethereum.ts b/packages/utils/src/ethereum.ts similarity index 81% rename from packages/bridge/src/ethereum.ts rename to packages/utils/src/ethereum.ts index f37522ace7..7c12a29689 100644 --- a/packages/bridge/src/ethereum.ts +++ b/packages/utils/src/ethereum.ts @@ -17,15 +17,43 @@ import { polygonMumbai, } from "viem/chains"; -import { SourceChain } from "./chain"; +/** + * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, + * to be handled similarly to other ERC20 tokens. + */ +export const NativeEVMTokenConstantAddress = + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + +/** Human-displayable global source chain identifiers. + * TODO: use global chain IDs instead of display names as keys + */ +export type AxelarSourceChain = + | "Bitcoin" + | "Bitcoin Testnet" + | "Aurora Testnet" + | "Avalanche" + | "Avalanche Fuji Testnet" + | "Binance Smart Chain" + | "BSC Testnet" + | "Ethereum" + | "Goerli Testnet" + | "Fantom" + | "Fantom Testnet" + | "Moonbeam" + | "Moonbase Alpha" + | "Polygon" + | "Mumbai" + | "Filecoin" + | "Filecoin Hyperspace" + | "Arbitrum"; -// TODO maybe we can use EVM chain ID (numeric) or ethereum chain registry +// TODO: maybe we can use EVM chain ID (numeric) or ethereum chain registry const createEthereumChainInfo = < Dict extends Partial< Record< - SourceChain, + AxelarSourceChain, Chain & { - chainName: SourceChain; + chainName: AxelarSourceChain; clientChainId: string; } > @@ -40,7 +68,7 @@ const mapChainInfo = ({ clientChainId, }: { chain: Chain; - axelarChainName: SourceChain; + axelarChainName: AxelarSourceChain; clientChainId: string; }) => ({ ...chain, @@ -125,10 +153,3 @@ export const EthereumChainInfo = createEthereumChainInfo({ clientChainId: "Filecoin Hyperspace", }), }); - -/** - * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, - * to be handled similarly to other ERC20 tokens. - */ -export const NativeEVMTokenConstantAddress = - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 68ead12a25..99ff9306d9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,6 +9,7 @@ export * from "./coin"; export * from "./common-utils"; export * from "./compare"; export * from "./date"; +export * from "./ethereum"; export * from "./function"; export * from "./gas-utils"; export * from "./ibc-utils"; diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 6427853591..1316008c4d 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -24,7 +24,7 @@ import { trimPlaceholderZeros } from "~/utils/number"; import { api } from "~/utils/trpc"; interface AmountScreenProps { - type: "deposit" | "withdraw"; + direction: "deposit" | "withdraw"; selectedDenom: string; /** @@ -34,7 +34,7 @@ interface AmountScreenProps { } export const AmountScreen = observer( - ({ type, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { + ({ direction, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { const { accountStore } = useStore(); const wallet = accountStore.getWallet(accountStore.osmosisChainId); const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); @@ -48,32 +48,33 @@ export const AmountScreen = observer( noop ); + const [sourceAsset, setSourceAsset] = useState(); + const [receiveAsset, setReceiveAsset] = useState(); + const [fromChain, setFromChain] = useState(); + const [toChain, setToChain] = useState(); + + const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); + const [cryptoAmount, setCryptoAmount] = useState("0"); + const [fiatAmount, setFiatAmount] = useState("0"); + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "osmosis", - }); - const { data: nobleChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "noble", + findChainNameOrId: accountStore.osmosisChainId, }); - const [receiveAsset, setReceiveAsset] = useState(); - const canonicalAsset = assetsInOsmosis?.[0]; const { price: assetInOsmosisPrice, isLoading: isLoadingCanonicalAssetPrice, } = usePrice(receiveAsset); - const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); - const [cryptoAmount, setCryptoAmount] = useState("0"); - const [fiatAmount, setFiatAmount] = useState("0"); - - const { supportedAssets, supportedChains } = useBridgesSupportedAssets({ - assets: assetsInOsmosis, - chain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - }); + const { supportedAssetsByChainId, supportedChains } = + useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); useEffect(() => { if (!isNil(assetsInOsmosis) && setReceiveAsset) { @@ -84,14 +85,45 @@ export const AmountScreen = observer( } }, [assetsInOsmosis, selectedDenom]); + useEffect(() => { + if (isNil(toChain) && !isNil(osmosisChain)) { + setToChain({ + chainId: osmosisChain.chain_id, + chainName: osmosisChain.pretty_name, + chainType: "cosmos", + }); + } + }, [accountStore.osmosisChainId, osmosisChain, toChain]); + + useEffect(() => { + if (isNil(toChain) && !isNil(osmosisChain)) { + setToChain({ + chainId: osmosisChain.chain_id, + chainName: osmosisChain.pretty_name, + chainType: "cosmos", + }); + } + }, [accountStore.osmosisChainId, osmosisChain, toChain]); + + useEffect(() => { + if (isNil(fromChain) && !isNil(supportedChains)) { + const firstChain = supportedChains[0]; + setFromChain({ + chainId: firstChain.chainId, + chainName: firstChain.prettyName, + chainType: firstChain.chainType, + } as BridgeChain); + } + }, [fromChain, supportedChains]); + if ( isLoadingCanonicalAssetPrice || - isNil(supportedAssets) || + isNil(supportedAssetsByChainId) || !assetsInOsmosis || !canonicalAsset || !assetInOsmosisPrice || - !osmosisChain || - !nobleChain + !fromChain || + !toChain ) { return ; } @@ -110,6 +142,11 @@ export const AmountScreen = observer( new Dec(fiatAmount === "" ? 0 : fiatAmount) ); + const supportedAssets = + supportedAssetsByChainId[ + direction === "deposit" ? fromChain.chainId : toChain.chainId + ]; + const parseFiatAmount = (value: string) => { return value.replace("$", ""); }; @@ -161,7 +198,7 @@ export const AmountScreen = observer(
- {type === "deposit" + {direction === "deposit" ? t("transfer.deposit") : t("transfer.withdraw")} {" "} @@ -189,15 +226,25 @@ export const AmountScreen = observer( {}} + onSelectChain={(nextChain) => { + setFromChain(nextChain); + }} + readonly={direction === "withdraw"} > - {nobleChain.pretty_name} + {fromChain.chainName} - - {osmosisChain.pretty_name} + { + setToChain(nextChain); + }} + readonly={direction === "deposit"} + > + {toChain.chainName}
@@ -270,35 +317,32 @@ export const AmountScreen = observer(
-
- {[ - { - label: "USDC.e", - amount: `$80.00 ${t("transfer.available")}`, - active: true, - }, - { label: "USDC", amount: "$30.00", active: false }, - { label: "USDC.axl", amount: "$10.00", active: false }, - ].map(({ label, amount, active }, index) => ( - - ))} -
+ {supportedAssets.length > 1 && ( +
+ {supportedAssets.map(({ denom }, index) => { + const isActive = denom === supportedAssets[0].denom; + return ( + + ); + })} +
+ )}
- {type === "deposit" + {direction === "deposit" ? t("transfer.transferWith") : t("transfer.transferTo")} @@ -370,7 +414,7 @@ export const AmountScreen = observer( }; const isConvert = - asset.coinDenom === asset.variantGroupKey; + asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = receiveAsset?.coinDenom === asset.coinDenom; @@ -461,7 +505,7 @@ export const AmountScreen = observer( ) : ( <> @@ -470,12 +514,12 @@ export const AmountScreen = observer( className="w-full text-lg font-h6 text-wosmongton-200 hover:text-white-full" onClick={() => setIsMoreOptionsVisible(true)} > - {type === "deposit" + {direction === "deposit" ? t("transfer.moreDepositOptions") : t("transfer.moreWithdrawOptions")} ["supportedChains"]; - onSelectChain?: (chain: BridgeChain) => void; + chains: ReturnType["supportedChains"]; + onSelectChain: (chain: BridgeChain) => void; }> = ({ readonly, children, chainLogo: _chainLogo, chains, onSelectChain }) => { const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); diff --git a/packages/web/components/bridge/immersive/asset-select-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx index a5e49a7979..8ee2cdb08f 100644 --- a/packages/web/components/bridge/immersive/asset-select-screen.tsx +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -24,10 +24,9 @@ import { UnverifiedAssetsState } from "~/stores/user-settings/unverified-assets" import { formatPretty } from "~/utils/formatter"; import { api, RouterOutputs } from "~/utils/trpc"; -const variantsNotToBeExcluded = ["WBTC", "BTC"] satisfies ( - | MainnetVariantGroupKeys - | TestnetVariantGroupKeys -)[]; +const variantsNotToBeExcluded = [ + "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", +] satisfies (MainnetVariantGroupKeys | TestnetVariantGroupKeys)[]; const prioritizedDenoms = [ "USDC", "OSMO", @@ -137,9 +136,9 @@ export const AssetSelectScreen = observer(
{isLoading ? ( - <> +
- +
) : ( <> {assets.map((asset) => ( diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index ba3c4c1ebd..3cf78f82de 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -35,6 +35,9 @@ export const BridgeNetworkSelect = ({ title={t("transfer.bridgeNetworkSelect.title")} className="!max-w-[30rem]" {...modalProps} + onAfterClose={() => { + setQuery(""); + }} > { @@ -50,17 +53,11 @@ export const BridgeNetworkSelect = ({ key={chain.chainId} className="subtitle1 flex items-center justify-between rounded-2xl px-4 py-4 transition-colors duration-200 hover:bg-osmoverse-700/50" onClick={() => - onSelectChain( - chain.chainType === "cosmos" - ? { - chainId: String(chain.chainId), - chainType: chain.chainType, - } - : { - chainId: Number(chain.chainId), - chainType: chain.chainType, - } - ) + onSelectChain({ + chainId: chain.chainId, + chainType: chain.chainType, + chainName: chain.prettyName, + } as BridgeChain) } > {chain.prettyName} diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 318fb5bee2..454cde7d91 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -194,7 +194,7 @@ export const ImmersiveBridgeFlow = ({ {() => ( diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts index 6e1eab8ffb..cec53e641a 100644 --- a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -62,33 +62,123 @@ export const useBridgesSupportedAssets = ({ [supportedAssetsResults] ); - const supportedAssets = useMemo(() => { - return successfulQueries.reduce((acc, { data }) => { + /** + * Aggregate supported assets from all successful queries. + * This would be an object with chain id as key and an array of supported assets as value. + * + * Example: + * { + * 1: [ + * { + * "chainId": 1, + * "chainType": "evm", + * "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + * "denom": "USDC", + * "decimals": 6, + * "sourceDenom": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + * "supportedVariants": [ + * "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + * "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", + * "ibc/231FD77ECCB2DB916D314019DA30FE013202833386B1908A191D16989AD80B5A", + * "ibc/F17C9CA112815613C5B6771047A093054F837C3020CBA59DFFD9D780A8B2984C", + * "ibc/9F9B07EF9AD291167CF5700628145DE1DEB777C2CFC7907553B24446515F6D0E", + * "ibc/6B99DB46AA9FF47162148C1726866919E44A6A5E0274B90912FD17E19A337695", + * "ibc/F08DE332018E8070CC4C68FE06E04E254F527556A614F5F8F9A68AF38D367E45" + * ], + * "supportedProviders": [ + * "Skip", + * "Squid", + * "Axelar" + * ] + * } + * ] + * } + */ + const supportedAssetsByChainId = useMemo(() => { + /** + * Map of supported assets by asset address. This is used to + * merge the supported variants for each input asset. + */ + const assetAddress_supportedVariants: Record> = {}; + + /** + * Map of supported assets by asset address. This is used to + * merge the supported providers for each input asset. + */ + const assetAddress_supportedProviders: Record> = {}; + + type AssetsByChainId = + RouterOutputs["bridgeTransfer"]["getSupportedAssetsByBridge"]["supportedAssets"]["assetsByChainId"]; + + const allAssetsByChainId = successfulQueries.reduce((acc, { data }) => { if (!data) return acc; // Merge all assets from providers by chain id - Object.entries(data.supportedAssets.assets).forEach(([key, value]) => { - if (acc[key]) { - acc[key] = acc[key].concat(value); - } else { - acc[key] = value; - } - }); - - // Remove duplicates - Object.keys(acc).forEach((key) => { - if (Array.isArray(acc[key])) { - acc[key] = acc[key].filter( - (value, index, self) => - index === self.findIndex((t) => t.address === value.address) - ); + Object.entries(data.supportedAssets.assetsByChainId).forEach( + ([chainId, assets]) => { + assets.forEach((asset) => { + const { address: rawAddress } = asset; + // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid + const address = rawAddress.toLowerCase(); + + if (!assetAddress_supportedVariants[address]) { + assetAddress_supportedVariants[address] = new Set(); + } + if (!assetAddress_supportedProviders[address]) { + assetAddress_supportedProviders[address] = new Set(); + } + assetAddress_supportedVariants[address].add( + data.supportedAssets.inputAssetAddress + ); + assetAddress_supportedProviders[address].add( + data.supportedAssets.providerName + ); + }); + + acc[chainId] = acc[chainId] ? [...acc[chainId], ...assets] : assets; } - }); + ); return acc; - }, {} as RouterOutputs["bridgeTransfer"]["getSupportedAssetsByBridge"]["supportedAssets"]["assets"]); + }, {} as AssetsByChainId); + + const assetEntriesByChainId = Object.entries(allAssetsByChainId).map( + ([chainId, assets]) => [ + chainId, + assets + .map((asset) => ({ + ...asset, + providerName: undefined, + supportedVariants: Array.from( + assetAddress_supportedVariants[asset.address.toLowerCase()] + ), + supportedProviders: Array.from( + assetAddress_supportedProviders[asset.address.toLowerCase()] + ), + })) + // Remove Duplicates + .filter( + (asset, index, originalArray) => + index === + // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid + originalArray.findIndex( + (t) => t.address.toLowerCase() === asset.address.toLowerCase() + ) + ), + ] + ); + + return Object.fromEntries(assetEntriesByChainId) as Record< + keyof AssetsByChainId, + (AssetsByChainId[string][number] & { + supportedVariants: string[]; + supportedProviders: Bridge[]; + })[] + >; }, [successfulQueries]); + console.log(supportedAssetsByChainId); + const supportedChains = useMemo(() => { return Array.from( // Remove duplicate chains @@ -103,5 +193,5 @@ export const useBridgesSupportedAssets = ({ ); }, [successfulQueries]); - return { supportedAssets, supportedChains }; + return { supportedAssetsByChainId, supportedChains }; }; diff --git a/packages/web/config/generate-lists.ts b/packages/web/config/generate-lists.ts index 6c7b58d900..f330e42612 100644 --- a/packages/web/config/generate-lists.ts +++ b/packages/web/config/generate-lists.ts @@ -268,7 +268,13 @@ async function generateAssetListFile({ // remove duplicates return self.indexOf(groupKey) === index; }) - .map((groupKey) => `"${groupKey}"`) + .map( + (groupKey) => + `"${groupKey}" /** Symbols: ${assetList.assets + .filter((asset) => asset.variantGroupKey === groupKey)! + .map((asset) => asset.symbol) + .join(",")} */` + ) .join(" | ")}; `; diff --git a/packages/web/integrations/ethereum/types.ts b/packages/web/integrations/ethereum/types.ts index fd029e90b5..928d1df2f7 100644 --- a/packages/web/integrations/ethereum/types.ts +++ b/packages/web/integrations/ethereum/types.ts @@ -51,11 +51,4 @@ export const ChainNames: { [chainId: string]: string } = { "0x4e454153": "Aurora Testnet", }; -/** - * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, - * to be handled similarly to other ERC20 tokens. - */ -export const NativeEVMTokenConstantAddress = - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; - export const ChainNetworkConfigs: { [chainId: string]: object } = {}; diff --git a/packages/web/modals/bridge-transfer-v2.tsx b/packages/web/modals/bridge-transfer-v2.tsx index cdbd41f899..1d99c9b79b 100644 --- a/packages/web/modals/bridge-transfer-v2.tsx +++ b/packages/web/modals/bridge-transfer-v2.tsx @@ -6,12 +6,15 @@ import type { CosmosBridgeTransactionRequest, EvmBridgeTransactionRequest, GetTransferStatusParams, - SourceChain, SourceChainTokenConfig, } from "@osmosis-labs/bridge"; import { DeliverTxResponse } from "@osmosis-labs/stores"; import { Currency } from "@osmosis-labs/types"; -import { getKeyByValue } from "@osmosis-labs/utils"; +import { + AxelarSourceChain, + getKeyByValue, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import { noop } from "@osmosis-labs/utils"; import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; @@ -45,7 +48,6 @@ import { import { ChainNames, EthWallet, - NativeEVMTokenConstantAddress, useErc20Balance, useNativeBalance, useTxReceiptState, @@ -63,7 +65,7 @@ interface BridgeTransferContext { useNativeToken: boolean; setUseWrappedToken: (nextValue: boolean) => void; sourceChainConfig?: SourceChainTokenConfig; - sourceChainKeyMapped: SourceChain; + sourceChainKeyMapped: AxelarSourceChain; originCurrency: Currency; } @@ -76,7 +78,7 @@ const [BridgeTransferModalProvider, useBridgeTransfer] = interface BridgeTransferModalProps extends ModalBaseProps { isWithdraw: boolean; balance: IBCBalance; - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; walletClient?: ObservableWallet; onRequestSwitchWallet: () => void; } @@ -321,7 +323,7 @@ export const TransferContent: FunctionComponent< isWithdraw: boolean; balance: IBCBalance; /** Selected network key. */ - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; onRequestSwitchWallet: () => void; counterpartyAddress: string; isCounterpartyAddressValid?: boolean; diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 397a2edc68..aafb9b78c3 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -217,17 +217,20 @@ export const bridgeTransferRouter = createTRPCRouter({ const supportedAssetFn = () => bridgeProvider.getSupportedAssets(input); - /** If the bridge takes longer than 10 seconds to respond, we should timeout that quote. */ + /** If the bridge takes longer than 10 seconds to respond, we should timeout that query. */ const supportedAssets = await timeout(supportedAssetFn, 10 * 1000)(); const assetsByChainId = supportedAssets.reduce< - Record + Record< + BridgeChain["chainId"], + ((typeof supportedAssets)[number] & { providerName: string })[] + > >((acc, asset) => { if (!acc[asset.chainId]) { acc[asset.chainId] = []; } - acc[asset.chainId].push(asset); + acc[asset.chainId].push({ ...asset, providerName: input.bridge }); return acc; }, {}); @@ -260,10 +263,13 @@ export const bridgeTransferRouter = createTRPCRouter({ chainType, }; } else if (chainType === "cosmos") { - const cosmosChain = getChain({ - chainList: ctx.chainList, - chainNameOrId: String(chainId), - }); + let cosmosChain: ReturnType | undefined; + try { + cosmosChain = getChain({ + chainList: ctx.chainList, + chainNameOrId: String(chainId), + }); + } catch {} if (!cosmosChain) { return undefined; @@ -282,11 +288,9 @@ export const bridgeTransferRouter = createTRPCRouter({ return { supportedAssets: { - provider: { - id: bridgeProvider.providerName as Bridge, - logoUrl: BridgeLogoUrls[bridgeProvider.providerName as Bridge], - }, - assets: assetsByChainId, + providerName: bridgeProvider.providerName as Bridge, + inputAssetAddress: input.asset.address, + assetsByChainId, availableChains, }, }; From 2e8524e8683c3352338bde57cbace98319487169 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Wed, 26 Jun 2024 19:27:35 -0400 Subject: [PATCH 13/33] feat: add local bridge transfer router --- .../__tests__/axelar-bridge-provider.spec.ts | 2 +- packages/bridge/src/axelar/index.ts | 3 +- packages/bridge/src/axelar/tokens.ts | 4 +- packages/bridge/src/index.ts | 2 - .../src/queries/complex/assets/ethereum.ts | 37 +- .../src/queries/complex/assets/index.ts | 1 + packages/trpc/src/local-assets.ts | 41 -- packages/trpc/src/parameter-types.ts | 10 + .../bridge/immersive/amount-screen.tsx | 602 +++++++++++++----- .../bridge/immersive/immersive-bridge.tsx | 53 +- .../immersive/supported-assets-list.tsx | 139 ++++ .../immersive/use-bridges-supported-assets.ts | 17 +- packages/web/server/api/local-router.ts | 3 + .../web/server/api/routers/bridge-transfer.ts | 3 +- .../api/routers/local-bridge-transfer.ts | 136 ++++ 15 files changed, 801 insertions(+), 252 deletions(-) delete mode 100644 packages/trpc/src/local-assets.ts create mode 100644 packages/web/components/bridge/immersive/supported-assets-list.tsx create mode 100644 packages/web/server/api/routers/local-bridge-transfer.ts diff --git a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts index 237f75d685..485dbe46ef 100644 --- a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts +++ b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts @@ -4,6 +4,7 @@ import { AxelarQueryAPI, } from "@axelar-network/axelarjs-sdk"; import { estimateGasFee } from "@osmosis-labs/tx"; +import { NativeEVMTokenConstantAddress } from "@osmosis-labs/utils"; import { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; // eslint-disable-next-line import/no-extraneous-dependencies @@ -11,7 +12,6 @@ import { rest } from "msw"; import { MockAssetLists } from "../../__tests__/mock-asset-lists"; import { server } from "../../__tests__/msw"; -import { NativeEVMTokenConstantAddress } from "../../ethereum"; import { BridgeProviderContext } from "../../interface"; import { AxelarBridgeProvider } from "../index"; import { diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index ced70a0bd9..39fded10a0 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -8,9 +8,11 @@ import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import type { IbcTransferMethod } from "@osmosis-labs/types"; import { + EthereumChainInfo, getAssetFromAssetList, getKeyByValue, isNil, + NativeEVMTokenConstantAddress, } from "@osmosis-labs/utils"; import { cachified } from "cachified"; import { @@ -23,7 +25,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, diff --git a/packages/bridge/src/axelar/tokens.ts b/packages/bridge/src/axelar/tokens.ts index e5231b731a..07b8b89562 100644 --- a/packages/bridge/src/axelar/tokens.ts +++ b/packages/bridge/src/axelar/tokens.ts @@ -1,10 +1,10 @@ -import { EthereumChainInfo, SourceChain } from "@osmosis-labs/utils"; +import { AxelarSourceChain, EthereumChainInfo } from "@osmosis-labs/utils"; import { BridgeEnvironment } from "../interface"; export type SourceChainTokenConfig = { /** Source Chain identifier. */ - id: SourceChain; + id: AxelarSourceChain; chainId?: number; /** Address of origin ERC20 token for that origin chain. Leave blank to * prefer native ETH currency if `id` is not a Cosmos chain in `ChainInfo`. diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index 5355c23b94..a4cb7494d4 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -1,8 +1,6 @@ export * from "./axelar"; export * from "./bridge-providers"; -export * from "./chain"; export * from "./errors"; -export * from "./ethereum"; export * from "./interface"; export * from "./skip"; export * from "./squid"; diff --git a/packages/server/src/queries/complex/assets/ethereum.ts b/packages/server/src/queries/complex/assets/ethereum.ts index 84c7cc6303..6a58174409 100644 --- a/packages/server/src/queries/complex/assets/ethereum.ts +++ b/packages/server/src/queries/complex/assets/ethereum.ts @@ -1,9 +1,40 @@ -import { NativeEVMTokenConstantAddress } from "@osmosis-labs/utils"; +import { + EthereumChainInfo, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; +import { Address, createPublicClient, erc20Abi, http } from "viem"; -export function getEvmBalance({ address, userAddress, chainId }: { +export async function getEvmBalance({ + address, + userAddress, + chainId, +}: { address: string; userAddress: string; chainId: number; }) { - if(address === NativeEVMTokenConstantAddress) + const evmChain = Object.values(EthereumChainInfo).find( + (chain) => String(chain.id) === String(chainId) + ); + + if (!evmChain) { + throw new Error(`Chain with id ${chainId} not found`); + } + + const publicClient = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); + + const balance = + address === NativeEVMTokenConstantAddress + ? await publicClient.getBalance({ address: userAddress as Address }) + : await publicClient.readContract({ + abi: erc20Abi, + address: address as Address, + functionName: "balanceOf", + args: [userAddress as Address], + }); + + return balance; } diff --git a/packages/server/src/queries/complex/assets/index.ts b/packages/server/src/queries/complex/assets/index.ts index 114a440c67..3b30028b77 100644 --- a/packages/server/src/queries/complex/assets/index.ts +++ b/packages/server/src/queries/complex/assets/index.ts @@ -193,6 +193,7 @@ function filterAssetList( export * from "./bridge"; export * from "./categories"; export * from "./config"; +export * from "./ethereum"; export * from "./gas"; export * from "./market"; export * from "./price"; diff --git a/packages/trpc/src/local-assets.ts b/packages/trpc/src/local-assets.ts deleted file mode 100644 index e60d5d921e..0000000000 --- a/packages/trpc/src/local-assets.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getAsset, getAssetWithUserBalance } from "@osmosis-labs/server"; -import { z } from "zod"; - -import { createTRPCRouter, publicProcedure } from "./api"; -import { UserOsmoAddressSchema } from "./parameter-types"; - -const evmSchema = z.object({ - type: z.literal("evm"), - address: z.string().startsWith("0x"), - userAddress: z.string(), - chainId: z.number(), -}); - -const cosmosSchema = z - .object({ - type: z.literal("cosmos"), - anyDenom: z.string(), - }) - .merge(UserOsmoAddressSchema.required()); - -export const localAssetsRouter = createTRPCRouter({ - getBalance: publicProcedure - .input(z.union([evmSchema, cosmosSchema])) - .query(async ({ input, ctx }) => { - if (input.type === "evm") { - } - - if (input.type === "cosmos") { - const asset = getAsset({ - ...ctx, - anyDenom: input.anyDenom, - }); - - return await getAssetWithUserBalance({ - ...ctx, - asset, - userOsmoAddress: input.userOsmoAddress, - }); - } - }), -}); diff --git a/packages/trpc/src/parameter-types.ts b/packages/trpc/src/parameter-types.ts index 7027ceaa24..9b52702e10 100644 --- a/packages/trpc/src/parameter-types.ts +++ b/packages/trpc/src/parameter-types.ts @@ -7,3 +7,13 @@ export type UserOsmoAddress = z.infer; export const UserOsmoAddressSchema = z.object({ userOsmoAddress: z.string().startsWith("osmo").optional(), }); + +export type UserCosmosAddress = z.infer; +export const UserCosmosAddressSchema = z.object({ + userCosmosAddress: z.string().optional(), +}); + +export type UserEvmAddress = z.infer; +export const UserEvmAddressSchema = z.object({ + userEvmAddress: z.string().startsWith("0x").optional(), +}); diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 1316008c4d..1989aaa1b4 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,6 +1,6 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; -import { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeAsset, BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { MinimalAsset } from "@osmosis-labs/types"; import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; @@ -17,11 +17,16 @@ import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; -import { useConnectWalletModalRedirect, useTranslation } from "~/hooks"; +import { + useConnectWalletModalRedirect, + useTranslation, + useWalletSelect, +} from "~/hooks"; +import { useEvmWalletAccount } from "~/hooks/evm-wallet"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; import { trimPlaceholderZeros } from "~/utils/number"; -import { api } from "~/utils/trpc"; +import { api, RouterOutputs } from "~/utils/trpc"; interface AmountScreenProps { direction: "deposit" | "withdraw"; @@ -36,7 +41,8 @@ interface AmountScreenProps { export const AmountScreen = observer( ({ direction, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { const { accountStore } = useStore(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); + const { onOpenWalletSelect } = useWalletSelect(); + const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); const { t } = useTranslation(); @@ -48,8 +54,8 @@ export const AmountScreen = observer( noop ); - const [sourceAsset, setSourceAsset] = useState(); - const [receiveAsset, setReceiveAsset] = useState(); + const [sourceAsset, setSourceAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); @@ -57,15 +63,54 @@ export const AmountScreen = observer( const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); + // Wallets + const account = accountStore.getWallet(accountStore.osmosisChainId); + const { address: evmAddress, connector: evmConnector } = + useEvmWalletAccount(); + + const sourceChain = direction === "deposit" ? fromChain : toChain; + const destinationChain = direction === "deposit" ? toChain : fromChain; + + const cosmosCounterpartyAccountRepo = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWalletRepo(sourceChain.chainId); + const cosmosCounterpartyAccount = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWallet(sourceChain.chainId); + + const currentAddress = + sourceChain?.chainType === "evm" + ? evmAddress + : cosmosCounterpartyAccount?.address; + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ findChainNameOrId: accountStore.osmosisChainId, }); const canonicalAsset = assetsInOsmosis?.[0]; + const { price: assetInOsmosisPrice, isLoading: isLoadingCanonicalAssetPrice, - } = usePrice(receiveAsset); + } = usePrice( + destinationAsset && sourceAsset && sourceChain + ? { + coinMinimalDenom: + sourceChain.chainType === "evm" + ? /** + * Use the destination osmosis asset to determine the price of the ETH asset. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + destinationAsset.address + : sourceAsset.address, + } + : undefined + ); const { supportedAssetsByChainId, supportedChains } = useBridgesSupportedAssets({ @@ -76,49 +121,162 @@ export const AmountScreen = observer( }, }); - useEffect(() => { - if (!isNil(assetsInOsmosis) && setReceiveAsset) { - // TODO: Get canonical asset from supported assets - setReceiveAsset( - assetsInOsmosis.find((asset) => asset.coinDenom === selectedDenom)! - ); + const supportedAssets = + supportedAssetsByChainId[sourceChain?.chainId ?? ""]; + + const { + data: sourceAssetsBalances, + isLoading: isLoadingSourceAssetsBalance, + } = api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( + sourceChain?.chainType === "evm" + ? { + type: "evm", + assets: supportedAssets as Extract< + SupportedAsset, + { chainType: "evm" } + >[], + userEvmAddress: evmAddress, + } + : { + type: "cosmos", + assets: supportedAssets as Extract< + SupportedAsset, + { chainType: "cosmos" } + >[], + userCosmosAddress: cosmosCounterpartyAccount?.address, + }, + { + enabled: + !isNil(sourceChain) && + !isNil(supportedAssets) && + (sourceChain?.chainType === "cosmos" + ? !isNil(cosmosCounterpartyAccount?.address) + : !isNil(evmAddress)), + + select: (data) => { + let nextData: typeof data = data; + + // Filter out assets with no balance + if (nextData) { + const filteredData = nextData.filter((asset) => + asset.amount.toDec().gt(new Dec(0)) + ); + + // If there are no assets with balance, leave one to be selected + if (filteredData.length === 0) { + nextData = [nextData[0]]; + } else { + nextData = filteredData; + } + } + + if (!sourceAsset && nextData) { + const highestBalance = nextData.reduce( + (acc, curr) => + curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, + nextData[0] + ); + + setSourceAsset({ + address: highestBalance.amount.currency.coinMinimalDenom, + decimals: highestBalance.amount.currency.coinDecimals, + denom: highestBalance.coinDenom, + sourceDenom: highestBalance.amount.currency.coinMinimalDenom, + }); + } + + return nextData; + }, } - }, [assetsInOsmosis, selectedDenom]); + ); useEffect(() => { - if (isNil(toChain) && !isNil(osmosisChain)) { - setToChain({ - chainId: osmosisChain.chain_id, - chainName: osmosisChain.pretty_name, - chainType: "cosmos", + if ( + !isNil(supportedAssets) && + !isNil(sourceAsset) && + !isNil(assetsInOsmosis) + ) { + const selectedSupportedAsset = supportedAssets.find( + (a) => a.address === sourceAsset.address + )!; + const destinationAsset = assetsInOsmosis.find( + (a) => a.coinDenom === selectedSupportedAsset.supportedVariants[0] + )!; + + setDestinationAsset({ + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + denom: destinationAsset.coinDenom, + sourceDenom: destinationAsset.sourceDenom, }); } - }, [accountStore.osmosisChainId, osmosisChain, toChain]); + }, [assetsInOsmosis, selectedDenom, sourceAsset, supportedAssets]); useEffect(() => { - if (isNil(toChain) && !isNil(osmosisChain)) { - setToChain({ + const chain = direction === "deposit" ? toChain : fromChain; + const setChain = direction === "deposit" ? setToChain : setFromChain; + if (isNil(chain) && !isNil(osmosisChain)) { + setChain({ chainId: osmosisChain.chain_id, chainName: osmosisChain.pretty_name, chainType: "cosmos", }); } - }, [accountStore.osmosisChainId, osmosisChain, toChain]); + }, [ + accountStore.osmosisChainId, + direction, + fromChain, + osmosisChain, + toChain, + ]); useEffect(() => { - if (isNil(fromChain) && !isNil(supportedChains)) { + const chain = direction === "deposit" ? fromChain : toChain; + const setChain = direction === "deposit" ? setFromChain : setToChain; + if ( + isNil(chain) && + !isNil(supportedChains) && + supportedChains.length > 0 + ) { const firstChain = supportedChains[0]; - setFromChain({ + setChain({ chainId: firstChain.chainId, chainName: firstChain.prettyName, chainType: firstChain.chainType, } as BridgeChain); } - }, [fromChain, supportedChains]); + }, [direction, fromChain, supportedChains, toChain]); + + useEffect(() => { + if (!fromChain || !toChain) return; + + const chain = direction === "deposit" ? fromChain : toChain; + if ( + typeof chain.chainId !== "string" || + !!cosmosCounterpartyAccount?.address + ) { + return; + } + cosmosCounterpartyAccountRepo?.connect(account?.walletName).catch(() => + onOpenWalletSelect({ + walletOptions: [ + { walletType: "cosmos", chainId: String(chain.chainId) }, + ], + }) + ); + }, [ + cosmosCounterpartyAccount?.address, + cosmosCounterpartyAccountRepo, + direction, + fromChain, + onOpenWalletSelect, + toChain, + account?.walletName, + ]); if ( isLoadingCanonicalAssetPrice || - isNil(supportedAssetsByChainId) || + isNil(supportedAssets) || !assetsInOsmosis || !canonicalAsset || !assetInOsmosisPrice || @@ -128,6 +286,16 @@ export const AmountScreen = observer( return ; } + const supportedReceiveTokens = supportedAssets.find( + ({ address }) => address === destinationAsset?.address + )?.supportedVariants; + + console.log( + supportedAssets, + destinationAsset?.address, + supportedReceiveTokens + ); + const cryptoAmountPretty = new CoinPretty( canonicalAsset, cryptoAmount === "" @@ -142,11 +310,6 @@ export const AmountScreen = observer( new Dec(fiatAmount === "" ? 0 : fiatAmount) ); - const supportedAssets = - supportedAssetsByChainId[ - direction === "deposit" ? fromChain.chainId : toChain.chainId - ]; - const parseFiatAmount = (value: string) => { return value.replace("$", ""); }; @@ -317,28 +480,59 @@ export const AmountScreen = observer(
- {supportedAssets.length > 1 && ( -
- {supportedAssets.map(({ denom }, index) => { - const isActive = denom === supportedAssets[0].denom; - return ( - - ); - })} -
- )} + <> + {isLoadingSourceAssetsBalance && ( +
+ +

Looking for balances

+
+ )} + + {!isLoadingSourceAssetsBalance && + sourceAssetsBalances?.length === 1 && ( +
+

+ {sourceAssetsBalances[0].usdValue.toString()} available +

+
+ )} + + {!isLoadingSourceAssetsBalance && + (sourceAssetsBalances?.length ?? 0) > 1 && ( +
+ {(sourceAssetsBalances ?? []).map((asset) => { + const isActive = + asset.amount.currency.coinMinimalDenom === + sourceAsset?.address; + return ( + + ); + })} +
+ )} +
@@ -347,12 +541,27 @@ export const AmountScreen = observer( : t("transfer.transferTo")}
- {wallet?.walletInfo.prettyName} - {wallet?.walletInfo.prettyName} + {sourceChain?.chainType === "evm" ? ( + <> + {!isNil(evmConnector?.icon) && ( + {evmConnector?.name} + )} + {evmConnector?.name} + + ) : ( + <> + {account?.walletInfo.prettyName} + {account?.walletInfo.prettyName} + + )}
- {!isNil(assetsInOsmosis) && assetsInOsmosis.length > 1 && ( - - {({ open }) => ( -
- -
-
- - {t("transfer.receiveAsset")} - - -

- {t("transfer.receiveAsset")} -

-

- {t("transfer.receiveAssetDescription")} -

-
- } - > - - -
- -
- - {receiveAsset?.coinDenom} - - 0 && ( + + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

+ {t("transfer.receiveAsset")} +

+

+ {t("transfer.receiveAssetDescription")} +

+
} - )} - /> + > + + +
+ +
+ + {destinationAsset?.denom} + + +
-
-
- - - {assetsInOsmosis.map((asset, index) => { - const onClick = () => { - setReceiveAsset(asset); - }; - - const isConvert = - asset.coinMinimalDenom === asset.variantGroupKey; - const isSelected = - receiveAsset?.coinDenom === asset.coinDenom; - - // Is canonical asset - if (index === 0) { + + + + {supportedReceiveTokens.map((assetDenom, index) => { + const asset = assetsInOsmosis.find( + (asset) => asset.coinDenom === assetDenom + )!; + + const onClick = () => { + setDestinationAsset({ + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + denom: asset.coinDenom, + sourceDenom: asset.coinDenom, + }); + }; + + const isConvert = + asset.coinMinimalDenom === asset.variantGroupKey; + const isSelected = + destinationAsset?.denom === asset.coinDenom; + + // Is canonical asset + if (index === 0) { + return ( + + + + ); + } + return ( ); - } - - return ( - - - - ); - })} - -
- )} -
- )} + })} + +
+ )} + + )}
@@ -596,3 +815,48 @@ const AmountScreenSkeletonLoader = () => {
); }; + +type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; + +interface SupportedAssetListProps { + assets: NonNullable< + RouterOutputs["local"]["bridgeTransfer"]["getSupportedAssetsBalances"] + >; + selectedAssetAddress: string; + chain: BridgeChain; + onSelectAsset: (assetAddress: string) => void; +} + +export const SupportedAssetList: FunctionComponent = + observer(({ assets, selectedAssetAddress, onSelectAsset }) => { + return ( +
+ {assets.map((asset) => { + const isActive = + asset.amount.currency.coinMinimalDenom === selectedAssetAddress; + return ( + + ); + })} +
+ ); + }); diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 454cde7d91..8d0ddeac8b 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -12,8 +12,13 @@ import { StepProgress } from "~/components/stepper/progress-bar"; import { Button, IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; import { BridgeFlowProvider } from "~/hooks/bridge"; +import { + useDisconnectEvmWallet, + useEvmWalletAccount, +} from "~/hooks/evm-wallet"; import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; import { useDisclosure } from "~/hooks/use-disclosure"; +import { useWalletSelect } from "~/hooks/use-wallet-select"; import { FiatRampKey } from "~/integrations"; import { ModalCloseButton } from "~/modals"; import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; @@ -67,9 +72,9 @@ export const ImmersiveBridgeFlow = ({ onClose: onCloseFiatOnrampSelection, } = useDisclosure(); - // const { isConnected, address } = useEvmWalletAccount(); - // const { onOpenWalletSelect } = useWalletSelect(); - // const { disconnect } = useDisconnectEvmWallet(); + const { isConnected, address } = useEvmWalletAccount(); + const { onOpenWalletSelect } = useWalletSelect(); + const { disconnect } = useDisconnectEvmWallet(); useLockBodyScroll(isVisible); @@ -210,27 +215,27 @@ export const ImmersiveBridgeFlow = ({ )}
- {/* {isConnected ? ( -
-

Evm Address: {address}

- -
- ) : ( - - )} */} + {isConnected ? ( +
+

Evm Address: {address}

+ +
+ ) : ( + + )}
)} diff --git a/packages/web/components/bridge/immersive/supported-assets-list.tsx b/packages/web/components/bridge/immersive/supported-assets-list.tsx new file mode 100644 index 0000000000..3f8f0c001b --- /dev/null +++ b/packages/web/components/bridge/immersive/supported-assets-list.tsx @@ -0,0 +1,139 @@ +import { BridgeChain } from "@osmosis-labs/bridge"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import { FunctionComponent, useEffect } from "react"; + +import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; +import { useWalletSelect } from "~/hooks"; +import { useEvmWalletAccount } from "~/hooks/evm-wallet"; +import { useStore } from "~/stores"; +import { api, RouterOutputs } from "~/utils/trpc"; + +type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; + +interface BaseSupportedAssetListProps { + supportedAssets: SupportedAsset[]; + selectedAssetAddress: string; + onSelectAsset: (asset: SupportedAsset) => void; +} + +interface SupportedAssetEvmListProps extends BaseSupportedAssetListProps { + chain: Extract; +} + +interface SupportedAssetCosmosListProps extends BaseSupportedAssetListProps { + chain: Extract; +} + +export const SupportedAssetEvmList = (props: SupportedAssetEvmListProps) => { + const { address: evmAddress } = useEvmWalletAccount(); + + return ; +}; + +export const SupportedAssetCosmosList = observer( + (props: SupportedAssetCosmosListProps) => { + const { chain, supportedAssets } = props; + + const { accountStore } = useStore(); + const { onOpenWalletSelect } = useWalletSelect(); + const wallet = accountStore.getWallet(accountStore.osmosisChainId); + + const counterpartyAccountRepo = accountStore.getWalletRepo(chain.chainId); + const counterpartyAccount = accountStore.getWallet(chain.chainId); + + // TODO: Move this to root + useEffect(() => { + if (typeof chain.chainId !== "string" || !!counterpartyAccount?.address) + return; + counterpartyAccountRepo?.connect(wallet?.walletName).catch(() => + onOpenWalletSelect({ + walletOptions: [{ walletType: "cosmos", chainId: chain.chainId }], + }) + ); + }, [ + chain.chainId, + counterpartyAccount?.address, + counterpartyAccountRepo, + onOpenWalletSelect, + wallet?.walletName, + ]); + + const { data: assets, isLoading } = + api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( + { + type: "cosmos", + assets: supportedAssets as Extract< + SupportedAsset, + { chainType: "cosmos" } + >[], + userOsmoAddress: counterpartyAccount?.address!, + }, + { + enabled: !!counterpartyAccount?.address, + } + ); + + if (!assets) return null; + + return ( + + ); + } +); + +interface SupportedAssetInnerListProps extends BaseSupportedAssetListProps { + assets: NonNullable< + RouterOutputs["local"]["bridgeTransfer"]["getSupportedAssetsBalances"] + >; + isLoading: boolean; +} + +const SupportedAssetInnerList: FunctionComponent< + SupportedAssetInnerListProps +> = ({ + supportedAssets, + selectedAssetAddress, + onSelectAsset, + assets, + isLoading, +}) => { + return ( +
+ {assets.map((asset, index) => { + const isActive = + asset.amount.currency.coinMinimalDenom === selectedAssetAddress; + return ( + + ); + })} +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts index cec53e641a..2bd7b1e5ff 100644 --- a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -156,14 +156,19 @@ export const useBridgesSupportedAssets = ({ assetAddress_supportedProviders[asset.address.toLowerCase()] ), })) - // Remove Duplicates + .filter( (asset, index, originalArray) => + // Make sure the asset has at least one supported variant + asset.supportedVariants.length > 0 && + // Make sure the asset has at least one supported provider + asset.supportedProviders.length > 0 && + // Remove Duplicates index === - // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid - originalArray.findIndex( - (t) => t.address.toLowerCase() === asset.address.toLowerCase() - ) + // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid + originalArray.findIndex( + (t) => t.address.toLowerCase() === asset.address.toLowerCase() + ) ), ] ); @@ -177,8 +182,6 @@ export const useBridgesSupportedAssets = ({ >; }, [successfulQueries]); - console.log(supportedAssetsByChainId); - const supportedChains = useMemo(() => { return Array.from( // Remove duplicate chains diff --git a/packages/web/server/api/local-router.ts b/packages/web/server/api/local-router.ts index f9647c7257..c978947a59 100644 --- a/packages/web/server/api/local-router.ts +++ b/packages/web/server/api/local-router.ts @@ -7,6 +7,8 @@ import { swapRouter, } from "@osmosis-labs/trpc"; +import { localBridgeTransferRouter } from "~/server/api/routers/local-bridge-transfer"; + /** * This section includes tRPC functions that execute on the client-side. * Caution: Ensure no sensitive data is exposed through these functions. */ @@ -16,4 +18,5 @@ export const localRouter = createTRPCRouter({ concentratedLiquidity: concentratedLiquidityRouter, oneClickTrading: oneClickTradingRouter, cms: cmsRouter, + bridgeTransfer: localBridgeTransferRouter, }); diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index aafb9b78c3..20d3a26d84 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -5,7 +5,6 @@ import { BridgeCoin, BridgeProviders, bridgeSupportedAssetsSchema, - EthereumChainInfo, getBridgeExternalUrlSchema, getBridgeQuoteSchema, } from "@osmosis-labs/bridge"; @@ -16,7 +15,7 @@ import { getTimeoutHeight, } from "@osmosis-labs/server"; import { createTRPCRouter, publicProcedure } from "@osmosis-labs/trpc"; -import { isNil, timeout } from "@osmosis-labs/utils"; +import { EthereumChainInfo, isNil, timeout } from "@osmosis-labs/utils"; import { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; import { z } from "zod"; diff --git a/packages/web/server/api/routers/local-bridge-transfer.ts b/packages/web/server/api/routers/local-bridge-transfer.ts new file mode 100644 index 0000000000..92124bb6ee --- /dev/null +++ b/packages/web/server/api/routers/local-bridge-transfer.ts @@ -0,0 +1,136 @@ +import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; +import { + calcAssetValue, + captureErrorAndReturn, + DEFAULT_VS_CURRENCY, + getAsset, + getEvmBalance, + mapGetAssetsWithUserBalances, +} from "@osmosis-labs/server"; +import { + createTRPCRouter, + publicProcedure, + UserCosmosAddressSchema, + UserEvmAddressSchema, +} from "@osmosis-labs/trpc"; +import { getAddress } from "viem"; +import { z } from "zod"; + +export const localBridgeTransferRouter = createTRPCRouter({ + getSupportedAssetsBalances: publicProcedure + .input( + z.union([ + z + .object({ + type: z.literal("evm"), + assets: z.array( + z.object({ + chainId: z.number(), + denom: z.string(), + address: z.string(), + decimals: z.number(), + supportedVariants: z.array(z.string()).min(1), + }) + ), + }) + .merge(UserEvmAddressSchema), + + z + .object({ + type: z.literal("cosmos"), + assets: z.array( + z.object({ + chainId: z.string(), + denom: z.string(), + address: z.string(), + decimals: z.number(), + }) + ), + }) + .merge(UserCosmosAddressSchema), + ]) + ) + .query(async ({ input, ctx }) => { + if (input.type === "evm") { + input.assets.map(async (asset) => { + const emptyBalance = { + coinDenom: asset.denom, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + + if (!input.userEvmAddress) return emptyBalance; + + const balance = await getEvmBalance({ + address: getAddress(asset.address), + userAddress: input.userEvmAddress, + chainId: asset.chainId, + }).catch(() => undefined); + + if (!balance) return emptyBalance; + + const decAmount = new Dec(balance.toString()); + /** + * Use the supported variant to determine the price of the ETH asset. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: asset.supportedVariants[0], + amount: decAmount, + }).catch((e) => captureErrorAndReturn(e, undefined)); + + return { + coinDenom: asset.denom, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + decAmount + ), + usdValue: + usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + }); + } + + if (input.type === "cosmos") { + const assets = input.assets.map((asset) => + getAsset({ ...ctx, anyDenom: asset.denom }) + ); + + if (!input.userOsmoAddress) + return assets.map((asset) => ({ + coinDenom: asset.coinDenom, + amount: new CoinPretty(asset, new Dec(0)), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + })); + + const balances = await mapGetAssetsWithUserBalances({ + ...ctx, + assets, + userOsmoAddress: input.userOsmoAddress, + includePreview: true, + }); + + return balances.map((asset) => ({ + coinDenom: asset.coinDenom, + amount: asset.amount ?? new CoinPretty(asset, new Dec(0)), + usdValue: + asset.usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + })); + } + }), +}); From 1e60d72632e7105cc4e924ded916929b8c31dd37 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Wed, 26 Jun 2024 22:53:06 -0400 Subject: [PATCH 14/33] feat: use supported asset as source asset --- packages/bridge/src/interface.ts | 7 +- packages/bridge/src/skip/index.ts | 4 +- .../server/src/queries/complex/assets/user.ts | 28 +- packages/trpc/src/assets.ts | 10 +- .../bridge/immersive/amount-screen.tsx | 261 +++++++----------- .../bridge/immersive/more-bridge-options.tsx | 65 ++--- .../immersive/use-bridges-supported-assets.ts | 14 +- .../api/routers/local-bridge-transfer.ts | 192 +++++++------ 8 files changed, 282 insertions(+), 299 deletions(-) diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index 3f9345dcb0..d66a9941c8 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -139,7 +139,10 @@ const evmChainSchema = z.object({ chainType: z.literal("evm"), }); -const bridgeChainSchema = z.union([cosmosChainSchema, evmChainSchema]); +export const bridgeChainSchema = z.discriminatedUnion("chainType", [ + cosmosChainSchema, + evmChainSchema, +]); export type BridgeChain = z.infer; @@ -154,7 +157,7 @@ export interface BridgeStatus { maintenanceMessage?: string; } -const bridgeAssetSchema = z.object({ +export const bridgeAssetSchema = z.object({ /** * The denomination of the asset. */ diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index 26997122a7..ca3aaf5092 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -777,9 +777,9 @@ export class SkipBridgeProvider implements BridgeProvider { const url = new URL("https://ibc.fun/"); url.searchParams.set("src_chain", String(fromChain.chainId)); - url.searchParams.set("src_asset", fromAsset.address); + url.searchParams.set("src_asset", fromAsset.address.toLowerCase()); url.searchParams.set("dest_chain", String(toChain.chainId)); - url.searchParams.set("dest_asset", toAsset.address); + url.searchParams.set("dest_asset", toAsset.address.toLowerCase()); return { urlProviderName: "IBC.fun", url }; } diff --git a/packages/server/src/queries/complex/assets/user.ts b/packages/server/src/queries/complex/assets/user.ts index 85f7cade1b..8c8c3221a6 100644 --- a/packages/server/src/queries/complex/assets/user.ts +++ b/packages/server/src/queries/complex/assets/user.ts @@ -31,21 +31,24 @@ export async function getAssetWithUserBalance({ assetLists, chainList, asset, - userOsmoAddress, + userCosmosAddress, + chainId, }: { assetLists: AssetList[]; chainList: Chain[]; asset: TAsset; - userOsmoAddress?: string; + userCosmosAddress?: string; + chainId?: string; }): Promise { - if (!userOsmoAddress) return asset; + if (!userCosmosAddress) return asset; const userAssets = await mapGetAssetsWithUserBalances({ assetLists, chainList, assets: [asset], - userOsmoAddress, + userCosmosAddress: userCosmosAddress, includePreview: true, + chainId, }); return userAssets[0]; } @@ -57,19 +60,21 @@ export async function mapGetAssetsWithUserBalances< TAsset extends MinimalAsset >({ poolId, + chainId, ...params }: { assetLists: AssetList[]; chainList: Chain[]; assets?: TAsset[]; - userOsmoAddress?: string; + chainId?: string; + userCosmosAddress?: string; sortFiatValueDirection?: SortDirection; /** * If poolId is provided, only include assets that are part of the pool. */ poolId?: string; } & AssetFilter): Promise<(TAsset & MaybeUserAssetCoin)[]> { - const { userOsmoAddress, search, sortFiatValueDirection } = params; + const { userCosmosAddress, search, sortFiatValueDirection } = params; let { assets } = params; if (!assets) assets = getAssets(params) as TAsset[]; @@ -87,16 +92,21 @@ export async function mapGetAssetsWithUserBalances< ) as TAsset[]; } - if (!userOsmoAddress) return assets; + if (!userCosmosAddress) return assets; const { balances } = await queryBalances({ ...params, - bech32Address: userOsmoAddress, + // Defaults to Osmosis + chainId, + bech32Address: userCosmosAddress, }); const eventualUserAssets = assets .map(async (asset) => { - const balance = balances.find((a) => a.denom === asset.coinMinimalDenom); + const balance = balances.find( + (a) => + a.denom === asset.coinMinimalDenom || a.denom === asset.sourceDenom // If it's outside of Osmosis + ); // not a user asset if (!balance) return asset; diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index d6a4215d48..1c91abab70 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -55,7 +55,7 @@ export const assetsRouter = createTRPCRouter({ return await getAssetWithUserBalance({ ...ctx, asset, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, }); } ), @@ -87,7 +87,7 @@ export const assetsRouter = createTRPCRouter({ mapGetAssetsWithUserBalances({ ...ctx, search, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, onlyVerified, sortFiatValueDirection: "desc", includePreview, @@ -165,7 +165,7 @@ export const assetsRouter = createTRPCRouter({ const userAsset = await getAssetWithUserBalance({ ...ctx, asset, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, }); const userMarketAsset = await getMarketAsset({ asset: userAsset, @@ -290,7 +290,7 @@ export const assetsRouter = createTRPCRouter({ ...ctx, search, categories, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, includePreview, }); @@ -543,7 +543,7 @@ export const assetsRouter = createTRPCRouter({ ...ctx, search, // Only get balances for withdraw - userOsmoAddress: + userCosmosAddress: type === "withdraw" ? userOsmoAddress : undefined, sortFiatValueDirection: "desc", includePreview, diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 1989aaa1b4..e5580b4426 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,6 +1,6 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; -import { BridgeAsset, BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { MinimalAsset } from "@osmosis-labs/types"; import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; @@ -26,7 +26,11 @@ import { useEvmWalletAccount } from "~/hooks/evm-wallet"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; import { trimPlaceholderZeros } from "~/utils/number"; -import { api, RouterOutputs } from "~/utils/trpc"; +import { api } from "~/utils/trpc"; + +type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; interface AmountScreenProps { direction: "deposit" | "withdraw"; @@ -54,8 +58,8 @@ export const AmountScreen = observer( noop ); - const [sourceAsset, setSourceAsset] = useState(); - const [destinationAsset, setDestinationAsset] = useState(); + const [sourceAsset, setSourceAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); @@ -106,8 +110,8 @@ export const AmountScreen = observer( * * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. */ - destinationAsset.address - : sourceAsset.address, + destinationAsset.coinMinimalDenom + : sourceAsset.supportedVariants[0], } : undefined ); @@ -177,12 +181,7 @@ export const AmountScreen = observer( nextData[0] ); - setSourceAsset({ - address: highestBalance.amount.currency.coinMinimalDenom, - decimals: highestBalance.amount.currency.coinDecimals, - denom: highestBalance.coinDenom, - sourceDenom: highestBalance.amount.currency.coinMinimalDenom, - }); + setSourceAsset(highestBalance); } return nextData; @@ -191,26 +190,14 @@ export const AmountScreen = observer( ); useEffect(() => { - if ( - !isNil(supportedAssets) && - !isNil(sourceAsset) && - !isNil(assetsInOsmosis) - ) { - const selectedSupportedAsset = supportedAssets.find( - (a) => a.address === sourceAsset.address - )!; + if (!isNil(sourceAsset) && !isNil(assetsInOsmosis)) { const destinationAsset = assetsInOsmosis.find( - (a) => a.coinDenom === selectedSupportedAsset.supportedVariants[0] + (a) => a.coinMinimalDenom === sourceAsset.supportedVariants[0] )!; - setDestinationAsset({ - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, - sourceDenom: destinationAsset.sourceDenom, - }); + setDestinationAsset(destinationAsset); } - }, [assetsInOsmosis, selectedDenom, sourceAsset, supportedAssets]); + }, [assetsInOsmosis, selectedDenom, sourceAsset]); useEffect(() => { const chain = direction === "deposit" ? toChain : fromChain; @@ -286,16 +273,6 @@ export const AmountScreen = observer( return ; } - const supportedReceiveTokens = supportedAssets.find( - ({ address }) => address === destinationAsset?.address - )?.supportedVariants; - - console.log( - supportedAssets, - destinationAsset?.address, - supportedReceiveTokens - ); - const cryptoAmountPretty = new CoinPretty( canonicalAsset, cryptoAmount === "" @@ -514,16 +491,9 @@ export const AmountScreen = observer( "text-osmoverse-100": !isActive, } )} - onClick={() => - setSourceAsset({ - address: asset.amount.currency.coinMinimalDenom, - decimals: asset.amount.currency.coinDecimals, - denom: asset.coinDenom, - sourceDenom: asset.amount.currency.coinMinimalDenom, - }) - } + onClick={() => setSourceAsset(asset)} > - {asset.coinDenom} + {asset.denom} {asset.usdValue.toString()} @@ -571,71 +541,67 @@ export const AmountScreen = observer(
- {!isNil(supportedReceiveTokens) && - supportedReceiveTokens.length > 0 && ( - - {({ open }) => ( -
- -
-
- - {t("transfer.receiveAsset")} - - -

- {t("transfer.receiveAsset")} -

-

- {t("transfer.receiveAssetDescription")} -

-
+ {!isNil(sourceAsset) && sourceAsset.supportedVariants.length > 0 && ( + + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

+ {t("transfer.receiveAsset")} +

+

+ {t("transfer.receiveAssetDescription")} +

+
+ } + > + + +
+ +
+ + {destinationAsset?.coinDenom} + + - - -
- -
- - {destinationAsset?.denom} - - -
+ )} + />
- +
+
- - {supportedReceiveTokens.map((assetDenom, index) => { + + {sourceAsset.supportedVariants.map( + (variantCoinMinimalDenom, index) => { const asset = assetsInOsmosis.find( - (asset) => asset.coinDenom === assetDenom + (asset) => + asset.coinMinimalDenom === variantCoinMinimalDenom )!; const onClick = () => { - setDestinationAsset({ - address: asset.coinMinimalDenom, - decimals: asset.coinDecimals, - denom: asset.coinDenom, - sourceDenom: asset.coinDenom, - }); + setDestinationAsset(asset); }; const isConvert = asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = - destinationAsset?.denom === asset.coinDenom; + destinationAsset?.coinDenom === asset.coinDenom; // Is canonical asset if (index === 0) { @@ -643,8 +609,9 @@ export const AmountScreen = observer( ); - })} - -
- )} -
- )} + } + )} + +
+ )} + + )}
@@ -738,10 +707,31 @@ export const AmountScreen = observer( : t("transfer.moreWithdrawOptions")} setIsMoreOptionsVisible(false)} /> @@ -815,48 +805,3 @@ const AmountScreenSkeletonLoader = () => {
); }; - -type SupportedAsset = ReturnType< - typeof useBridgesSupportedAssets ->["supportedAssetsByChainId"][string][number]; - -interface SupportedAssetListProps { - assets: NonNullable< - RouterOutputs["local"]["bridgeTransfer"]["getSupportedAssetsBalances"] - >; - selectedAssetAddress: string; - chain: BridgeChain; - onSelectAsset: (assetAddress: string) => void; -} - -export const SupportedAssetList: FunctionComponent = - observer(({ assets, selectedAssetAddress, onSelectAsset }) => { - return ( -
- {assets.map((asset) => { - const isActive = - asset.amount.currency.coinMinimalDenom === selectedAssetAddress; - return ( - - ); - })} -
- ); - }); diff --git a/packages/web/components/bridge/immersive/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx index 02c01f3419..68f7d34bf0 100644 --- a/packages/web/components/bridge/immersive/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -1,4 +1,4 @@ -import { MinimalAsset } from "@osmosis-labs/types"; +import { BridgeAsset, BridgeChain } from "@osmosis-labs/bridge"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import React from "react"; @@ -7,57 +7,48 @@ import { Icon } from "~/components/assets"; import { SkeletonLoader } from "~/components/loaders"; import { useTranslation } from "~/hooks"; import { ModalBase, ModalBaseProps } from "~/modals"; -import { useStore } from "~/stores"; import { api } from "~/utils/trpc"; interface MoreBridgeOptionsProps extends ModalBaseProps { - type: "deposit" | "withdraw"; - asset: MinimalAsset; + direction: "deposit" | "withdraw"; + fromAsset: BridgeAsset | undefined; + toAsset: BridgeAsset | undefined; + fromChain: BridgeChain | undefined; + toChain: BridgeChain | undefined; + toAddress: string | undefined; } export const MoreBridgeOptions = observer( - ({ type, asset, ...modalProps }: MoreBridgeOptionsProps) => { - const { - accountStore, - chainStore: { - osmosis: { prettyChainName }, - }, - } = useStore(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); + ({ + direction, + fromAsset, + toAsset, + fromChain, + toChain, + toAddress, + ...modalProps + }: MoreBridgeOptionsProps) => { const { t } = useTranslation(); - // TODO: Get fromChain, toChain, and address from props after supportTokens. const { data: externalUrlsData, isLoading: isLoadingExternalUrls } = api.bridgeTransfer.getExternalUrls.useQuery( { - fromAsset: { - address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", - decimals: asset!.coinDecimals, - denom: asset!.coinDenom, - sourceDenom: asset!.sourceDenom, - }, - toAsset: { - address: asset!.coinMinimalDenom, - decimals: asset!.coinDecimals, - denom: asset!.coinDenom, - sourceDenom: asset!.sourceDenom, - }, - fromChain: { chainId: 43114, chainType: "evm" }, - toChain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - toAddress: wallet?.address ?? "", + fromAsset, + toAsset, + fromChain, + toChain, + toAddress, }, { - enabled: !!asset, + enabled: + !!fromAsset && !!toAsset && !!fromChain && !!toChain && !!toAddress, } ); return (

{t( - type === "deposit" + direction === "deposit" ? "transfer.moreBridgeOptions.descriptionDeposit" : "transfer.moreBridgeOptions.descriptionWithdraw", { - asset: asset?.coinDenom ?? "", - chain: prettyChainName, + asset: fromAsset.denom, + chain: toChain.chainName ?? "", } )}

@@ -102,7 +93,7 @@ export const MoreBridgeOptions = observer( /> {t( - type === "deposit" + direction === "deposit" ? "transfer.moreBridgeOptions.depositWith" : "transfer.moreBridgeOptions.withdrawWith" )}{" "} diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts index 2bd7b1e5ff..b2afccd991 100644 --- a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -146,9 +146,8 @@ export const useBridgesSupportedAssets = ({ ([chainId, assets]) => [ chainId, assets - .map((asset) => ({ + .map(({ providerName, ...asset }) => ({ ...asset, - providerName: undefined, supportedVariants: Array.from( assetAddress_supportedVariants[asset.address.toLowerCase()] ), @@ -175,10 +174,13 @@ export const useBridgesSupportedAssets = ({ return Object.fromEntries(assetEntriesByChainId) as Record< keyof AssetsByChainId, - (AssetsByChainId[string][number] & { - supportedVariants: string[]; - supportedProviders: Bridge[]; - })[] + Omit< + AssetsByChainId[string][number] & { + supportedVariants: string[]; + supportedProviders: Bridge[]; + }, + "providerName" + >[] >; }, [successfulQueries]); diff --git a/packages/web/server/api/routers/local-bridge-transfer.ts b/packages/web/server/api/routers/local-bridge-transfer.ts index 92124bb6ee..7717c1b145 100644 --- a/packages/web/server/api/routers/local-bridge-transfer.ts +++ b/packages/web/server/api/routers/local-bridge-transfer.ts @@ -1,11 +1,16 @@ import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; +import { + Bridge, + bridgeAssetSchema, + bridgeChainSchema, +} from "@osmosis-labs/bridge"; import { calcAssetValue, captureErrorAndReturn, DEFAULT_VS_CURRENCY, getAsset, + getAssetWithUserBalance, getEvmBalance, - mapGetAssetsWithUserBalances, } from "@osmosis-labs/server"; import { createTRPCRouter, @@ -19,32 +24,34 @@ import { z } from "zod"; export const localBridgeTransferRouter = createTRPCRouter({ getSupportedAssetsBalances: publicProcedure .input( - z.union([ + z.discriminatedUnion("type", [ z .object({ type: z.literal("evm"), assets: z.array( - z.object({ - chainId: z.number(), - denom: z.string(), - address: z.string(), - decimals: z.number(), - supportedVariants: z.array(z.string()).min(1), - }) + bridgeChainSchema.and(bridgeAssetSchema).and( + z.object({ + supportedVariants: z.array(z.string()), + supportedProviders: z.array( + z.string().transform((v) => v as Bridge) + ), + }) + ) ), }) .merge(UserEvmAddressSchema), - z .object({ type: z.literal("cosmos"), assets: z.array( - z.object({ - chainId: z.string(), - denom: z.string(), - address: z.string(), - decimals: z.number(), - }) + bridgeChainSchema.and(bridgeAssetSchema).and( + z.object({ + supportedVariants: z.array(z.string()), + supportedProviders: z.array( + z.string().transform((v) => v as Bridge) + ), + }) + ) ), }) .merge(UserCosmosAddressSchema), @@ -52,85 +59,110 @@ export const localBridgeTransferRouter = createTRPCRouter({ ) .query(async ({ input, ctx }) => { if (input.type === "evm") { - input.assets.map(async (asset) => { - const emptyBalance = { - coinDenom: asset.denom, - amount: new CoinPretty( - { - coinDecimals: asset.decimals, - coinDenom: asset.denom, - coinMinimalDenom: asset.address, - }, - new Dec(0) - ), - usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), - }; + input.assets + .filter( + (asset): asset is Extract => + asset.chainType !== "cosmos" + ) + .map(async (asset) => { + const emptyBalance = { + coinDenom: asset.denom, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; - if (!input.userEvmAddress) return emptyBalance; + if (!input.userEvmAddress) return emptyBalance; - const balance = await getEvmBalance({ - address: getAddress(asset.address), - userAddress: input.userEvmAddress, - chainId: asset.chainId, - }).catch(() => undefined); + const balance = await getEvmBalance({ + address: getAddress(asset.address), + userAddress: input.userEvmAddress, + chainId: asset.chainId, + }).catch(() => undefined); - if (!balance) return emptyBalance; + if (!balance) return emptyBalance; - const decAmount = new Dec(balance.toString()); - /** - * Use the supported variant to determine the price of the ETH asset. - * This is because providers can return variant assets that are missing in - * our asset list. - * - * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. - */ - const usdValue = await calcAssetValue({ - ...ctx, - anyDenom: asset.supportedVariants[0], - amount: decAmount, - }).catch((e) => captureErrorAndReturn(e, undefined)); + const decAmount = new Dec(balance.toString()); + /** + * Use the supported variant to determine the price of the ETH asset. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: asset.supportedVariants[0], + amount: decAmount, + }).catch((e) => captureErrorAndReturn(e, undefined)); - return { - coinDenom: asset.denom, + return { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + decAmount + ), + usdValue: + usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + }); + } + + if (input.type === "cosmos") { + if (!input.userCosmosAddress) { + return input.assets.map((asset) => ({ + ...asset, amount: new CoinPretty( { coinDecimals: asset.decimals, coinDenom: asset.denom, - coinMinimalDenom: asset.address, + coinMinimalDenom: asset.denom, }, - decAmount + new Dec(0) ), - usdValue: - usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), - }; - }); - } - - if (input.type === "cosmos") { - const assets = input.assets.map((asset) => - getAsset({ ...ctx, anyDenom: asset.denom }) - ); - - if (!input.userOsmoAddress) - return assets.map((asset) => ({ - coinDenom: asset.coinDenom, - amount: new CoinPretty(asset, new Dec(0)), usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), })); + } - const balances = await mapGetAssetsWithUserBalances({ - ...ctx, - assets, - userOsmoAddress: input.userOsmoAddress, - includePreview: true, - }); + const assetsWithBalance = await Promise.all( + input.assets + .filter( + ( + asset + ): asset is Extract => + asset.chainType !== "evm" + ) + .map(async (asset) => { + const assetWithBalance = await getAssetWithUserBalance({ + ...ctx, + userCosmosAddress: input.userCosmosAddress, + chainId: asset.chainId, + asset: getAsset({ ...ctx, anyDenom: asset.denom }), + }); + + return { + ...asset, + amount: + assetWithBalance.amount ?? + new CoinPretty(assetWithBalance, new Dec(0)), + usdValue: + assetWithBalance.usdValue ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + }) + ); - return balances.map((asset) => ({ - coinDenom: asset.coinDenom, - amount: asset.amount ?? new CoinPretty(asset, new Dec(0)), - usdValue: - asset.usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), - })); + return assetsWithBalance; } }), }); From bb6245b3976750e12b08da3a0660972f72811741 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Wed, 26 Jun 2024 23:06:42 -0400 Subject: [PATCH 15/33] improvement: type fix --- .../bridge/immersive/more-bridge-options.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/web/components/bridge/immersive/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx index 68f7d34bf0..36a07e839d 100644 --- a/packages/web/components/bridge/immersive/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -33,11 +33,11 @@ export const MoreBridgeOptions = observer( const { data: externalUrlsData, isLoading: isLoadingExternalUrls } = api.bridgeTransfer.getExternalUrls.useQuery( { - fromAsset, - toAsset, - fromChain, - toChain, - toAddress, + fromAsset: fromAsset!, + toAsset: toAsset!, + fromChain: fromChain!, + toChain: toChain!, + toAddress: toAddress!, }, { enabled: @@ -61,8 +61,8 @@ export const MoreBridgeOptions = observer( ? "transfer.moreBridgeOptions.descriptionDeposit" : "transfer.moreBridgeOptions.descriptionWithdraw", { - asset: fromAsset.denom, - chain: toChain.chainName ?? "", + asset: fromAsset?.denom ?? "", + chain: toChain?.chainName ?? "", } )}

From acdccecd18321ff3299e70b77ff77b140bf0a71f Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 27 Jun 2024 17:40:36 -0400 Subject: [PATCH 16/33] feat: handle wallets in immersive bridge --- .../bridge/immersive/amount-screen.tsx | 237 +++++++++------ .../immersive/bridge-network-select.tsx | 5 +- .../bridge/immersive/bridge-wallet-select.tsx | 273 ++++++++++++++++++ packages/web/localizations/en.json | 1 + .../modals/wallet-select/evm-wallet-state.tsx | 2 +- .../modals/wallet-select/full-wallet-list.tsx | 3 +- packages/web/modals/wallet-select/index.tsx | 225 ++------------- .../wallet-select/simple-wallet-list.tsx | 2 +- .../wallet-select/use-connect-wallet.ts | 214 ++++++++++++++ packages/web/modals/wallet-select/utils.ts | 19 +- packages/web/pages/test-bridge.tsx | 37 +++ .../api/routers/local-bridge-transfer.ts | 108 +++---- 12 files changed, 772 insertions(+), 354 deletions(-) create mode 100644 packages/web/components/bridge/immersive/bridge-wallet-select.tsx create mode 100644 packages/web/modals/wallet-select/use-connect-wallet.ts create mode 100644 packages/web/pages/test-bridge.tsx diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index e5580b4426..a05600ffe3 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -7,10 +7,17 @@ import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { FunctionComponent, ReactNode, useEffect, useState } from "react"; +import { + FunctionComponent, + ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import { Icon } from "~/components/assets"; import { BridgeNetworkSelect } from "~/components/bridge/immersive/bridge-network-select"; +import { BridgeWalletSelect } from "~/components/bridge/immersive/bridge-wallet-select"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; import { InputBox } from "~/components/input"; @@ -19,6 +26,7 @@ import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; import { useConnectWalletModalRedirect, + useDisclosure, useTranslation, useWalletSelect, } from "~/hooks"; @@ -66,6 +74,11 @@ export const AmountScreen = observer( const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); + const { + isOpen: isBridgeWalletSelectOpen, + onClose: onCloseBridgeWalletSelect, + onOpen: onOpenBridgeWalletSelect, + } = useDisclosure(); // Wallets const account = accountStore.getWallet(accountStore.osmosisChainId); @@ -99,21 +112,14 @@ export const AmountScreen = observer( price: assetInOsmosisPrice, isLoading: isLoadingCanonicalAssetPrice, } = usePrice( - destinationAsset && sourceAsset && sourceChain - ? { - coinMinimalDenom: - sourceChain.chainType === "evm" - ? /** - * Use the destination osmosis asset to determine the price of the ETH asset. - * This is because providers can return variant assets that are missing in - * our asset list. - * - * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. - */ - destinationAsset.coinMinimalDenom - : sourceAsset.supportedVariants[0], - } - : undefined + /** + * Use the canonical osmosis asset to determine the price of the assets. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + canonicalAsset ); const { supportedAssetsByChainId, supportedChains } = @@ -128,6 +134,19 @@ export const AmountScreen = observer( const supportedAssets = supportedAssetsByChainId[sourceChain?.chainId ?? ""]; + const supportedChainsAsBridgeChain = useMemo( + () => + supportedChains.map( + ({ chainId, chainType, prettyName }) => + ({ + chainId, + chainType, + chainName: prettyName, + } as BridgeChain) + ), + [supportedChains] + ); + const { data: sourceAssetsBalances, isLoading: isLoadingSourceAssetsBalance, @@ -150,12 +169,7 @@ export const AmountScreen = observer( userCosmosAddress: cosmosCounterpartyAccount?.address, }, { - enabled: - !isNil(sourceChain) && - !isNil(supportedAssets) && - (sourceChain?.chainType === "cosmos" - ? !isNil(cosmosCounterpartyAccount?.address) - : !isNil(evmAddress)), + enabled: !isNil(sourceChain) && !isNil(supportedAssets), select: (data) => { let nextData: typeof data = data; @@ -266,19 +280,25 @@ export const AmountScreen = observer( isNil(supportedAssets) || !assetsInOsmosis || !canonicalAsset || + !destinationAsset || !assetInOsmosisPrice || !fromChain || - !toChain + !toChain || + !sourceAsset ) { return ; } const cryptoAmountPretty = new CoinPretty( - canonicalAsset, + { + coinDecimals: sourceAsset.decimals, + coinDenom: sourceAsset.denom, + coinMinimalDenom: sourceAsset.address, + }, cryptoAmount === "" ? new Dec(0) : new Dec(cryptoAmount).mul( - DecUtils.getTenExponentN(canonicalAsset.coinDecimals) + DecUtils.getTenExponentN(sourceAsset.decimals) ) ); @@ -328,6 +348,11 @@ export const AmountScreen = observer( type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); }; + const resetAssets = () => { + setSourceAsset(undefined); + setDestinationAsset(undefined); + }; + const dropdownActiveItemIcon = (
@@ -368,6 +393,7 @@ export const AmountScreen = observer( chains={supportedChains} onSelectChain={(nextChain) => { setFromChain(nextChain); + resetAssets(); }} readonly={direction === "withdraw"} > @@ -381,6 +407,7 @@ export const AmountScreen = observer( chains={supportedChains} onSelectChain={(nextChain) => { setToChain(nextChain); + resetAssets(); }} readonly={direction === "deposit"} > @@ -469,7 +496,14 @@ export const AmountScreen = observer( sourceAssetsBalances?.length === 1 && (

- {sourceAssetsBalances[0].usdValue.toString()} available + {inputUnit === "crypto" + ? sourceAssetsBalances[0].amount + .trim(true) + .maxDecimals(6) + .hideDenom(true) + .toString() + : sourceAssetsBalances[0].usdValue.toString()}{" "} + available

)} @@ -495,7 +529,13 @@ export const AmountScreen = observer( > {asset.denom} - {asset.usdValue.toString()} + {inputUnit === "crypto" + ? asset.amount + .trim(true) + .maxDecimals(6) + .hideDenom(true) + .toString() + : asset.usdValue.toString()} ); @@ -504,42 +544,85 @@ export const AmountScreen = observer( )} -
- - {direction === "deposit" - ? t("transfer.transferWith") - : t("transfer.transferTo")} - -
- {sourceChain?.chainType === "evm" ? ( - <> - {!isNil(evmConnector?.icon) && ( - {evmConnector?.name} + {walletConnected && ( + <> + + + { + const setChain = + direction === "deposit" ? setFromChain : setToChain; + setChain(chain); + resetAssets(); + }} + evmChain={ + sourceChain?.chainType === "evm" + ? sourceChain + : supportedChainsAsBridgeChain.find( + ( + chain + ): chain is Extract< + BridgeChain, + { chainType: "evm" } + > => chain.chainType === "evm" + ) + } + cosmosChain={ + sourceChain?.chainType === "cosmos" + ? sourceChain + : supportedChainsAsBridgeChain.find( + ( + chain + ): chain is Extract< + BridgeChain, + { chainType: "cosmos" } + > => chain.chainType === "cosmos" + ) + } /> -
-
+ + )} {!isNil(sourceAsset) && sourceAsset.supportedVariants.length > 0 && ( @@ -586,7 +669,7 @@ export const AmountScreen = observer(
- + {sourceAsset.supportedVariants.map( (variantCoinMinimalDenom, index) => { const asset = assetsInOsmosis.find( @@ -598,7 +681,9 @@ export const AmountScreen = observer( setDestinationAsset(asset); }; + // Show all as deposit as for now const isConvert = + false ?? asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = destinationAsset?.coinDenom === asset.coinDenom; @@ -626,6 +711,8 @@ export const AmountScreen = observer(

{isConvert ? t("transfer.convertTo") + : direction === "withdraw" + ? t("transfer.withdrawAs") : t("transfer.depositAs")}{" "} {asset.coinDenom}

@@ -701,6 +788,7 @@ export const AmountScreen = observer( variant="ghost" className="w-full text-lg font-h6 text-wosmongton-200 hover:text-white-full" onClick={() => setIsMoreOptionsVisible(true)} + disabled={isNil(sourceAsset) || isNil(destinationAsset)} > {direction === "deposit" ? t("transfer.moreDepositOptions") @@ -709,26 +797,13 @@ export const AmountScreen = observer( { + onInput={(nextValue) => { setQuery(nextValue); - }, 300)} + }} className="my-4 flex-shrink-0" placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} size="full" diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx new file mode 100644 index 0000000000..054f423d6f --- /dev/null +++ b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx @@ -0,0 +1,273 @@ +import { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import React, { ReactNode, useState } from "react"; +import { Connector } from "wagmi"; + +import { SearchBox } from "~/components/input"; +import { Button } from "~/components/ui/button"; +import { EthereumChainIds } from "~/config/wagmi"; +import { + useDisconnectEvmWallet, + useEvmWalletAccount, +} from "~/hooks/evm-wallet"; +import { useTranslation } from "~/hooks/language"; +import { ModalBase, ModalBaseProps } from "~/modals"; +import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; +import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; +import { useStore } from "~/stores"; + +interface BridgeWalletSelectProps extends ModalBaseProps { + direction: BridgeTransactionDirection; + cosmosChain?: Extract; + evmChain?: Extract; + onSelectChain: (chain: BridgeChain) => void; +} + +export const BridgeWalletSelect = observer((props: BridgeWalletSelectProps) => { + const { direction, cosmosChain, evmChain, onSelectChain, ...modalProps } = + props; + + return ( + + + + ); +}); + +export const BridgeWalletSelectScreen = ({ + cosmosChain, + evmChain, + onClose, + onSelectChain, +}: Pick< + BridgeWalletSelectProps, + "cosmosChain" | "evmChain" | "direction" | "onSelectChain" +> & { + onClose: () => void; +}) => { + const { t } = useTranslation(); + const { accountStore } = useStore(); + const cosmosAccount = cosmosChain + ? accountStore.getWallet(cosmosChain.chainId) + : undefined; + + const [search, setSearch] = useState(""); + const [isManaging, setIsManaging] = useState(false); + + const { connector: evmConnector, isConnected: isEvmWalletConnected } = + useEvmWalletAccount(); + + const { disconnect: disconnectEvmWallet } = useDisconnectEvmWallet(); + + const { + onConnect: onConnectWallet, + wagmi: { + variables: wagmiVariables, + status: wagmiStatus, + error: wagmiError, + }, + } = useConnectWallet({ + walletOptions: [ + ...(cosmosChain?.chainId + ? [{ walletType: "cosmos" as const, chainId: cosmosChain.chainId }] + : []), + ...(evmChain?.chainId + ? [ + { + walletType: "evm" as const, + chainId: evmChain.chainId as EthereumChainIds, + }, + ] + : []), + ], + onConnect: () => { + onSelectChain(evmChain!); + onClose(); + }, + }); + const { evmWallets } = useSelectableWallets({ + includedWallets: ["evm"], + isMobile: false, + }); + + if (!isNil(wagmiVariables?.connector)) { + return ( +
+ +
+ ); + } + + const showEvmWallets = !isNil(evmChain) && !isNil(evmWallets); + + return ( +
+
+ {showEvmWallets && ( + { + setSearch(nextValue); + }} + currentValue={search} + /> + )} + + {(isEvmWalletConnected || !isNil(cosmosAccount)) && ( +
+
+

Your connected wallets

+ +
+ + <> + {!isNil(cosmosAccount) && !isNil(cosmosChain) && ( + { + onSelectChain(cosmosChain); + onClose(); + }} + name={ + isManaging ? ( + cosmosAccount.walletInfo.prettyName + ) : ( + <>Transfer from {cosmosAccount?.walletInfo.prettyName} + ) + } + icon={cosmosAccount.walletInfo.logo} + suffix={ + isManaging ? ( +

Primary wallet

+ ) : undefined + } + /> + )} + {isEvmWalletConnected && !isNil(evmChain) && ( + { + onSelectChain(evmChain); + onClose(); + }} + name={ + isManaging ? ( + evmConnector?.name + ) : ( + <>Transfer from {evmConnector?.name ?? ""} + ) + } + icon={evmConnector?.icon} + suffix={ + isManaging ? ( + + ) : undefined + } + /> + )} + +
+ )} + + {showEvmWallets && ( +
+

Other wallets

+
+ {evmWallets + .filter((wallet) => { + if (wallet.id === evmConnector?.id) return false; // Don't show connected wallet + if (!search) return true; + return wallet.name + .toLowerCase() + .includes(search.toLowerCase()); + }) + .map((wallet) => { + return ( + + onConnectWallet({ + walletType: "evm", + wallet, + chainId: evmChain.chainId as EthereumChainIds, + }) + } + name={wallet.name} + icon={wallet.icon} + /> + ); + })} +
+
+ )} +
+
+ ); +}; + +const WalletButton: React.FC<{ + onClick: () => void; + icon: string | undefined; + name: ReactNode; + suffix?: ReactNode; +}> = ({ onClick, icon, name, suffix }) => { + return ( +
+ + + {suffix} +
+ ); +}; diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index d401a5fd3b..7d0592300a 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -922,6 +922,7 @@ "moreWithdrawOptions": "More withdraw options", "convertTo": "Convert to", "depositAs": "Deposit as", + "withdrawAs": "Withdraw as", "recommended": "Recommended", "moreBridgeOptions": { "titleDeposit": "More deposit options", diff --git a/packages/web/modals/wallet-select/evm-wallet-state.tsx b/packages/web/modals/wallet-select/evm-wallet-state.tsx index 5fcbe81691..233162afb8 100644 --- a/packages/web/modals/wallet-select/evm-wallet-state.tsx +++ b/packages/web/modals/wallet-select/evm-wallet-state.tsx @@ -7,7 +7,7 @@ import { ErrorWalletState } from "~/components/wallet-states"; import { useTranslation } from "~/hooks"; import { ConnectEvmWalletReturn } from "~/hooks/evm-wallet"; import { WalletSelectModalProps } from "~/modals/wallet-select"; -import { OnConnectWallet } from "~/modals/wallet-select/utils"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; export const EvmWalletState: FunctionComponent< Pick & { diff --git a/packages/web/modals/wallet-select/full-wallet-list.tsx b/packages/web/modals/wallet-select/full-wallet-list.tsx index 070611c85f..693764ec38 100644 --- a/packages/web/modals/wallet-select/full-wallet-list.tsx +++ b/packages/web/modals/wallet-select/full-wallet-list.tsx @@ -4,8 +4,9 @@ import { observer } from "mobx-react-lite"; import React, { FunctionComponent, useMemo } from "react"; import { useTranslation, WalletSelectOption } from "~/hooks"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; -import { ModalView, OnConnectWallet } from "~/modals/wallet-select/utils"; +import { ModalView } from "~/modals/wallet-select/utils"; export const FullWalletList: FunctionComponent<{ walletRepo: WalletRepo | undefined; diff --git a/packages/web/modals/wallet-select/index.tsx b/packages/web/modals/wallet-select/index.tsx index 9e765c7fb7..d58f314dac 100644 --- a/packages/web/modals/wallet-select/index.tsx +++ b/packages/web/modals/wallet-select/index.tsx @@ -1,16 +1,6 @@ -import { - ChainWalletBase, - State, - WalletRepo, - WalletStatus, -} from "@cosmos-kit/core"; -import { - CosmosKitAccountsLocalStorageKey, - CosmosKitWalletLocalStorageKey, - CosmosRegistryWallet, -} from "@osmosis-labs/stores"; +import { State, WalletRepo, WalletStatus } from "@cosmos-kit/core"; import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; -import { isNil, noop } from "@osmosis-labs/utils"; +import { isNil } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import React, { FunctionComponent, useEffect, useState } from "react"; @@ -20,19 +10,9 @@ import { Connector } from "wagmi"; import { Icon } from "~/components/assets"; import { ClientOnly } from "~/components/client-only"; import { Button } from "~/components/ui/button"; -import { CosmosWalletRegistry } from "~/config"; -import { EthereumChainIds } from "~/config/wagmi"; -import { - useFeatureFlags, - WalletSelectOption, - WalletSelectParams, -} from "~/hooks"; +import { useFeatureFlags, WalletSelectParams } from "~/hooks"; import { useWindowSize } from "~/hooks"; -import { useConnectEvmWallet } from "~/hooks/evm-wallet"; -import { - CreateOneClickSessionError, - useCreateOneClickTradingSession, -} from "~/hooks/mutations/one-click-trading"; +import { useCreateOneClickTradingSession } from "~/hooks/mutations/one-click-trading"; import { useOneClickTradingParams } from "~/hooks/one-click-trading/use-one-click-trading-params"; import { useHasInstalledCosmosWallets } from "~/hooks/use-has-installed-wallets"; import { ModalBase, ModalBaseProps, ModalCloseButton } from "~/modals/base"; @@ -40,13 +20,8 @@ import { CosmosWalletState } from "~/modals/wallet-select/cosmos-wallet-state"; import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; import { FullWalletList } from "~/modals/wallet-select/full-wallet-list"; import { SimpleWalletList } from "~/modals/wallet-select/simple-wallet-list"; -import { WagmiWalletConnectType } from "~/modals/wallet-select/use-selectable-wallets"; -import { - getModalView, - ModalView, - OnConnectWallet, -} from "~/modals/wallet-select/utils"; -import { useStore } from "~/stores"; +import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { getModalView, ModalView } from "~/modals/wallet-select/utils"; export interface WalletSelectModalProps extends ModalBaseProps { /** @@ -67,18 +42,10 @@ export const WalletSelectModal: FunctionComponent = layout = "full", } = props; const { isMobile } = useWindowSize(); - const { accountStore, chainStore } = useStore(); const featureFlags = useFeatureFlags(); const hasInstalledWallets = useHasInstalledCosmosWallets(); const [show1CTEditParams, setShow1CTEditParams] = useState(false); const [hasBroadcastedTx, setHasBroadcastedTx] = useState(false); - const { - connectAsync: connectEvmWallet, - variables, - status, - error, - reset, - } = useConnectEvmWallet(); const create1CTSession = useCreateOneClickTradingSession({ onBroadcasted: () => { @@ -100,8 +67,7 @@ export const WalletSelectModal: FunctionComponent = const [modalView, setModalView] = useState("list"); const [isInitializingOneClickTrading, setIsInitializingOneClickTrading] = useState(false); - const [lazyWalletInfo, setLazyWalletInfo] = - useState<(typeof CosmosWalletRegistry)[number]>(); + const [show1CTConnectAWallet, setShow1CTConnectAWallet] = useState(false); const hasOneClickTradingError = !!create1CTSession.error; @@ -114,20 +80,23 @@ export const WalletSelectModal: FunctionComponent = reset: reset1CTParams, } = useOneClickTradingParams(); - const cosmosOption = walletOptions.find( - ( - option - ): option is Extract => - option.walletType === "cosmos" - ); + const { + onConnect: onConnectWallet, + wagmi: { variables, status, error, reset }, + cosmos: { walletRepo: rootWalletRepo, lazyWalletInfo }, + } = useConnectWallet({ + onConnecting: () => setModalView("connecting"), + onRequestClose: onRequestClose, + walletOptions, + onConnect: onConnectProp, - const cosmosChainId = cosmosOption?.chainId; - const rootWalletRepo = cosmosChainId - ? accountStore.getWalletRepo(cosmosChainId) - : undefined; - const current = rootWalletRepo?.current; - const cosmosWalletStatus = current?.walletStatus; - const cosmosChainName = rootWalletRepo?.chainRecord.chain?.chain_name!; + isOneClickEnabled: transaction1CTParams?.isOneClickEnabled, + onCreate1CTSession: ({ walletRepo }) => + onCreate1CTSession({ walletRepo, transaction1CTParams }), + }); + + const currentCosmosWallet = rootWalletRepo?.current; + const cosmosWalletStatus = currentCosmosWallet?.walletStatus; useEffect(() => { if (isOpen) { @@ -157,7 +126,7 @@ export const WalletSelectModal: FunctionComponent = } }, [isOpen]); - (current?.client as any)?.setActions?.({ + (currentCosmosWallet?.client as any)?.setActions?.({ qrUrl: { state: setQRState, /** @@ -196,144 +165,6 @@ export const WalletSelectModal: FunctionComponent = }); }; - const onConnectCosmosWallet = async ({ - wallet, - walletRepo: walletRepoParam, - }: { - wallet: CosmosRegistryWallet | ChainWalletBase; - walletRepo: WalletRepo; - }) => { - if (current) { - await current?.disconnect(true); - } - - const handleConnectError = (e: Error) => { - console.error("Error while connecting to wallet. Details: ", e); - localStorage.removeItem(CosmosKitWalletLocalStorageKey); - localStorage.removeItem(CosmosKitAccountsLocalStorageKey); - }; - - if (!("lazyInstall" in wallet)) { - wallet - .connect(false) - .then(() => { - onConnectProp?.({ walletType: "cosmos" }); - }) - .catch(handleConnectError); - return; - } - - const isWalletInstalled = rootWalletRepo?.wallets.some( - ({ walletName }) => walletName === wallet.name - ); - - let walletRepo: WalletRepo; - - // if wallet is not installed, install it - if (!isWalletInstalled && "lazyInstall" in wallet) { - setLazyWalletInfo(wallet); - setModalView("connecting"); - - // wallet is now walletInfo - const walletInfo = wallet; - const WalletClass = await wallet.lazyInstall(); - - const walletManager = await accountStore.addWallet( - new WalletClass(walletInfo) - ); - await walletManager.onMounted().catch(handleConnectError); - setLazyWalletInfo(undefined); - - walletRepo = walletManager.getWalletRepo(cosmosChainName!); - } else { - walletRepo = walletRepoParam; - } - - const isOsmosisConnection = - chainStore.osmosis.chainName === cosmosChainName!; - const osmosisWalletRepo = accountStore.getWalletRepo( - chainStore.osmosis.chainName - ); - - if ( - !isOsmosisConnection && - osmosisWalletRepo.walletStatus !== WalletStatus.Connected - ) { - await osmosisWalletRepo - .connect(wallet.name, false) - .catch(handleConnectError); - } - - return walletRepo - .connect(wallet.name, false) - .then(async () => { - onConnectProp?.({ walletType: "cosmos" }); - - if (transaction1CTParams?.isOneClickEnabled) { - try { - await onCreate1CTSession({ walletRepo, transaction1CTParams }); - } catch (e) { - const error = e as CreateOneClickSessionError | Error; - - if (error instanceof Error) { - throw new CreateOneClickSessionError(error.message); - } - - throw e; - } - } - }) - .catch((e: Error | unknown) => { - if (e instanceof CreateOneClickSessionError) throw e; - handleConnectError( - e instanceof Error ? e : new Error("Unknown error.") - ); - }); - }; - - const onConnectWagmiWallet = async ({ - wallet, - chainId, - }: { - wallet: Connector; - chainId: EthereumChainIds | undefined; - }) => { - // Close modal to show WalletConnect QR code modal - if (wallet.type === WagmiWalletConnectType) { - onRequestClose(); - } - - return connectEvmWallet( - { connector: wallet, chainId: chainId }, - { - onSuccess: () => { - onConnectProp?.({ walletType: "evm" }); - }, - onError: (e) => { - console.error("Error while connecting to wallet. Details: ", e); - }, - } - ); - }; - - const onConnect: OnConnectWallet = async (param) => { - if (!param.wallet) return; - - if (param.walletType === "cosmos" && rootWalletRepo) { - return onConnectCosmosWallet({ - wallet: param.wallet, - walletRepo: rootWalletRepo, - }); - } - - if (param.walletType === "evm") { - return onConnectWagmiWallet({ - wallet: param.wallet, - chainId: param.chainId, - }).catch(noop); - } - }; - const onRequestBack = modalView !== "list" ? () => { @@ -389,7 +220,7 @@ export const WalletSelectModal: FunctionComponent = @@ -400,7 +231,7 @@ export const WalletSelectModal: FunctionComponent = connector={variables.connector as Connector} status={status} error={error} - onConnect={onConnect} + onConnect={onConnectWallet} />
)} @@ -426,7 +257,7 @@ export const WalletSelectModal: FunctionComponent = )} > = {...props} onRequestClose={onClose} modalView={modalView} - onConnect={onConnect} + onConnect={onConnectWallet} lazyWalletInfo={lazyWalletInfo} transaction1CTParams={transaction1CTParams} setTransaction1CTParams={setTransaction1CTParams} diff --git a/packages/web/modals/wallet-select/simple-wallet-list.tsx b/packages/web/modals/wallet-select/simple-wallet-list.tsx index 5b8790e1ca..09d89dc654 100644 --- a/packages/web/modals/wallet-select/simple-wallet-list.tsx +++ b/packages/web/modals/wallet-select/simple-wallet-list.tsx @@ -4,8 +4,8 @@ import React, { FunctionComponent, useMemo, useState } from "react"; import { SearchBox } from "~/components/input"; import { useTranslation, WalletSelectOption } from "~/hooks"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; -import { OnConnectWallet } from "~/modals/wallet-select/utils"; export const SimpleWalletList: FunctionComponent<{ onConnect: OnConnectWallet; diff --git a/packages/web/modals/wallet-select/use-connect-wallet.ts b/packages/web/modals/wallet-select/use-connect-wallet.ts new file mode 100644 index 0000000000..606406d0ea --- /dev/null +++ b/packages/web/modals/wallet-select/use-connect-wallet.ts @@ -0,0 +1,214 @@ +import { ChainWalletBase, WalletRepo, WalletStatus } from "@cosmos-kit/core"; +import { + CosmosKitAccountsLocalStorageKey, + CosmosKitWalletLocalStorageKey, + CosmosRegistryWallet, +} from "@osmosis-labs/stores"; +import { noop } from "@osmosis-labs/utils"; +import { useState } from "react"; +import { Connector } from "wagmi"; + +import { EthereumChainIds } from "~/config/wagmi"; +import { CosmosWalletRegistry } from "~/config/wallet-registry"; +import { useConnectEvmWallet } from "~/hooks/evm-wallet"; +import { CreateOneClickSessionError } from "~/hooks/mutations/one-click-trading"; +import { WalletSelectOption } from "~/hooks/use-wallet-select"; +import { WagmiWalletConnectType } from "~/modals/wallet-select/use-selectable-wallets"; +import { useStore } from "~/stores"; + +export type OnConnectWallet = ( + params: + | { + walletType: "cosmos"; + wallet: CosmosRegistryWallet | ChainWalletBase | undefined; + } + | { + walletType: "evm"; + wallet: Connector; + chainId?: EthereumChainIds; + } +) => Promise; + +export const useConnectWallet = ({ + onRequestClose, + walletOptions, + onConnect: onConnectProp, + onConnecting, + onCreate1CTSession, + isOneClickEnabled, +}: { + onConnect?: (params: { walletType: "evm" | "cosmos" }) => void; + onConnecting?: () => void; + walletOptions: WalletSelectOption[]; + onRequestClose?: () => void; + + isOneClickEnabled?: boolean; + onCreate1CTSession?: (params: { walletRepo: WalletRepo }) => Promise; +}) => { + const { accountStore, chainStore } = useStore(); + + const { connectAsync: connectEvmWallet, ...connectEvmWalletUtils } = + useConnectEvmWallet(); + + const [lazyWalletInfo, setLazyWalletInfo] = + useState<(typeof CosmosWalletRegistry)[number]>(); + + const cosmosOption = walletOptions.find( + (option): option is Extract => + option.walletType === "cosmos" + ); + + const cosmosChainId = cosmosOption?.chainId; + const rootWalletRepo = cosmosChainId + ? accountStore.getWalletRepo(cosmosChainId) + : undefined; + const current = rootWalletRepo?.current; + const cosmosChainName = rootWalletRepo?.chainRecord.chain?.chain_name!; + + const onConnectCosmosWallet = async ({ + wallet, + walletRepo: walletRepoParam, + }: { + wallet: CosmosRegistryWallet | ChainWalletBase; + walletRepo: WalletRepo; + }) => { + if (current) { + await current?.disconnect(true); + } + + const handleConnectError = (e: Error) => { + console.error("Error while connecting to wallet. Details: ", e); + localStorage.removeItem(CosmosKitWalletLocalStorageKey); + localStorage.removeItem(CosmosKitAccountsLocalStorageKey); + }; + + if (!("lazyInstall" in wallet)) { + wallet + .connect(false) + .then(() => { + onConnectProp?.({ walletType: "cosmos" }); + }) + .catch(handleConnectError); + return; + } + + const isWalletInstalled = rootWalletRepo?.wallets.some( + ({ walletName }) => walletName === wallet.name + ); + + let walletRepo: WalletRepo; + + // if wallet is not installed, install it + if (!isWalletInstalled && "lazyInstall" in wallet) { + setLazyWalletInfo(wallet); + onConnecting?.(); + + // wallet is now walletInfo + const walletInfo = wallet; + const WalletClass = await wallet.lazyInstall(); + + const walletManager = await accountStore.addWallet( + new WalletClass(walletInfo) + ); + await walletManager.onMounted().catch(handleConnectError); + setLazyWalletInfo(undefined); + + walletRepo = walletManager.getWalletRepo(cosmosChainName!); + } else { + walletRepo = walletRepoParam; + } + + const isOsmosisConnection = + chainStore.osmosis.chainName === cosmosChainName!; + const osmosisWalletRepo = accountStore.getWalletRepo( + chainStore.osmosis.chainName + ); + + if ( + !isOsmosisConnection && + osmosisWalletRepo.walletStatus !== WalletStatus.Connected + ) { + await osmosisWalletRepo + .connect(wallet.name, false) + .catch(handleConnectError); + } + + return walletRepo + .connect(wallet.name, false) + .then(async () => { + onConnectProp?.({ walletType: "cosmos" }); + + if (isOneClickEnabled && onCreate1CTSession) { + try { + await onCreate1CTSession({ walletRepo }); + } catch (e) { + const error = e as CreateOneClickSessionError | Error; + + if (error instanceof Error) { + throw new CreateOneClickSessionError(error.message); + } + + throw e; + } + } + }) + .catch((e: Error | unknown) => { + if (e instanceof CreateOneClickSessionError) throw e; + handleConnectError( + e instanceof Error ? e : new Error("Unknown error.") + ); + }); + }; + + const onConnectWagmiWallet = async ({ + wallet, + chainId, + }: { + wallet: Connector; + chainId: EthereumChainIds | undefined; + }) => { + // Close modal to show WalletConnect QR code modal + if (wallet.type === WagmiWalletConnectType) { + onRequestClose?.(); + } + + await connectEvmWallet( + { connector: wallet, chainId: chainId }, + { + onSuccess: () => { + onConnectProp?.({ walletType: "evm" }); + }, + onError: (e) => { + console.error("Error while connecting to wallet. Details: ", e); + }, + } + ); + }; + + const onConnect: OnConnectWallet = async (param) => { + if (!param.wallet) return; + + if (param.walletType === "cosmos" && rootWalletRepo) { + return onConnectCosmosWallet({ + wallet: param.wallet, + walletRepo: rootWalletRepo, + }); + } + + if (param.walletType === "evm") { + return onConnectWagmiWallet({ + wallet: param.wallet, + chainId: param.chainId, + }).catch(noop); + } + }; + + return { + wagmi: connectEvmWalletUtils, + cosmos: { + lazyWalletInfo, + walletRepo: rootWalletRepo, + }, + onConnect, + }; +}; diff --git a/packages/web/modals/wallet-select/utils.ts b/packages/web/modals/wallet-select/utils.ts index a619ee547f..edf2225ea3 100644 --- a/packages/web/modals/wallet-select/utils.ts +++ b/packages/web/modals/wallet-select/utils.ts @@ -1,8 +1,4 @@ -import { ChainWalletBase, State, WalletStatus } from "@cosmos-kit/core"; -import { CosmosRegistryWallet } from "@osmosis-labs/stores"; -import { Connector } from "wagmi"; - -import { EthereumChainIds } from "~/config/wagmi"; +import { State, WalletStatus } from "@cosmos-kit/core"; export type ModalView = | "list" @@ -57,16 +53,3 @@ export function getModalView({ return "list"; } - -export type OnConnectWallet = ( - params: - | { - walletType: "cosmos"; - wallet: CosmosRegistryWallet | ChainWalletBase | undefined; - } - | { - walletType: "evm"; - wallet: Connector; - chainId?: EthereumChainIds; - } -) => void; diff --git a/packages/web/pages/test-bridge.tsx b/packages/web/pages/test-bridge.tsx new file mode 100644 index 0000000000..d5ace6a003 --- /dev/null +++ b/packages/web/pages/test-bridge.tsx @@ -0,0 +1,37 @@ +import { NextPage } from "next"; +import Image from "next/image"; +import { NextSeo } from "next-seo"; +import { useMount } from "react-use"; + +import { useTranslation } from "~/hooks"; +import { useBridge } from "~/hooks/bridge"; + +const TestBridge: NextPage = () => { + const { t } = useTranslation(); + const { bridgeAsset } = useBridge(); + + useMount(() => { + bridgeAsset({ + direction: "deposit", + anyDenom: "USDC", + }); + }); + + return ( +
+ + {t("404.title")} +
{t("404.title")}
+
+ ); +}; + +export default TestBridge; diff --git a/packages/web/server/api/routers/local-bridge-transfer.ts b/packages/web/server/api/routers/local-bridge-transfer.ts index 7717c1b145..7e0270c3ac 100644 --- a/packages/web/server/api/routers/local-bridge-transfer.ts +++ b/packages/web/server/api/routers/local-bridge-transfer.ts @@ -59,63 +59,67 @@ export const localBridgeTransferRouter = createTRPCRouter({ ) .query(async ({ input, ctx }) => { if (input.type === "evm") { - input.assets - .filter( - (asset): asset is Extract => - asset.chainType !== "cosmos" - ) - .map(async (asset) => { - const emptyBalance = { - coinDenom: asset.denom, - amount: new CoinPretty( - { - coinDecimals: asset.decimals, - coinDenom: asset.denom, - coinMinimalDenom: asset.address, - }, - new Dec(0) - ), - usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), - }; + return Promise.all( + input.assets + .filter( + (asset): asset is Extract => + asset.chainType !== "cosmos" + ) + .map(async (asset) => { + const emptyBalance = { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; - if (!input.userEvmAddress) return emptyBalance; + if (!input.userEvmAddress) return emptyBalance; - const balance = await getEvmBalance({ - address: getAddress(asset.address), - userAddress: input.userEvmAddress, - chainId: asset.chainId, - }).catch(() => undefined); + const balance = await getEvmBalance({ + address: getAddress(asset.address), + userAddress: input.userEvmAddress, + chainId: asset.chainId, + }).catch(() => undefined); - if (!balance) return emptyBalance; + if (!balance) return emptyBalance; - const decAmount = new Dec(balance.toString()); - /** - * Use the supported variant to determine the price of the ETH asset. - * This is because providers can return variant assets that are missing in - * our asset list. - * - * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. - */ - const usdValue = await calcAssetValue({ - ...ctx, - anyDenom: asset.supportedVariants[0], - amount: decAmount, - }).catch((e) => captureErrorAndReturn(e, undefined)); + const decAmount = new Dec(balance.toString()); + /** + * Use the supported variant to determine the price of the ETH asset. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: asset.supportedVariants[0], + amount: decAmount, + }).catch((e) => captureErrorAndReturn(e, undefined)); - return { - ...asset, - amount: new CoinPretty( - { - coinDecimals: asset.decimals, - coinDenom: asset.denom, - coinMinimalDenom: asset.address, - }, - decAmount - ), - usdValue: - usdValue ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), - }; - }); + return { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + decAmount + ), + usdValue: new PricePretty( + DEFAULT_VS_CURRENCY, + usdValue ?? new Dec(0) + ), + }; + }) + ); } if (input.type === "cosmos") { From 11e6be57c6431059b96645e12e083b985818fb92 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 27 Jun 2024 18:19:41 -0400 Subject: [PATCH 17/33] feat: handle connection edge cases --- .../bridge/immersive/amount-screen.tsx | 237 ++++++++++++------ 1 file changed, 162 insertions(+), 75 deletions(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index a05600ffe3..cd2de10380 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -147,6 +147,26 @@ export const AmountScreen = observer( [supportedChains] ); + const firstSupportedEvmChain = useMemo( + () => + supportedChainsAsBridgeChain.find( + (chain): chain is Extract => + chain.chainType === "evm" + ), + [supportedChainsAsBridgeChain] + ); + const firstSupportedCosmosChain = useMemo( + () => + supportedChainsAsBridgeChain.find( + (chain): chain is Extract => + chain.chainType === "cosmos" + ), + [supportedChainsAsBridgeChain] + ); + + const hasMoreThanOneChainType = + !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); + const { data: sourceAssetsBalances, isLoading: isLoadingSourceAssetsBalance, @@ -203,6 +223,9 @@ export const AmountScreen = observer( } ); + /** + * Set the initial destination asset based on the source asset. + */ useEffect(() => { if (!isNil(sourceAsset) && !isNil(assetsInOsmosis)) { const destinationAsset = assetsInOsmosis.find( @@ -213,6 +236,9 @@ export const AmountScreen = observer( } }, [assetsInOsmosis, selectedDenom, sourceAsset]); + /** + * Set the osmosis chain based on the direction + */ useEffect(() => { const chain = direction === "deposit" ? toChain : fromChain; const setChain = direction === "deposit" ? setToChain : setFromChain; @@ -231,6 +257,11 @@ export const AmountScreen = observer( toChain, ]); + /** + * Set the initial chain based on the direction. + * TODO: When all balances are computed per chain, we can default to the highest balance + * instead of the first one. + */ useEffect(() => { const chain = direction === "deposit" ? fromChain : toChain; const setChain = direction === "deposit" ? setFromChain : setToChain; @@ -248,17 +279,27 @@ export const AmountScreen = observer( } }, [direction, fromChain, supportedChains, toChain]); + /** + * Connect cosmos wallet to the counterparty chain + */ useEffect(() => { if (!fromChain || !toChain) return; const chain = direction === "deposit" ? fromChain : toChain; + if ( - typeof chain.chainId !== "string" || - !!cosmosCounterpartyAccount?.address + // If the chain is an EVM chain, we don't need to connect the cosmos chain + chain.chainType !== "cosmos" || + // Or if the account is already connected + !!cosmosCounterpartyAccount?.address || + // Or if there's no available cosmos chain + !firstSupportedCosmosChain ) { return; } + cosmosCounterpartyAccountRepo?.connect(account?.walletName).catch(() => + // Display the connect modal if the user for some reason rejects the connection onOpenWalletSelect({ walletOptions: [ { walletType: "cosmos", chainId: String(chain.chainId) }, @@ -266,13 +307,43 @@ export const AmountScreen = observer( }) ); }, [ + account?.walletName, cosmosCounterpartyAccount?.address, cosmosCounterpartyAccountRepo, direction, + firstSupportedCosmosChain, fromChain, onOpenWalletSelect, toChain, - account?.walletName, + ]); + + /** + * Connect evm wallet to the counterparty chain + */ + useEffect(() => { + if (!fromChain || !toChain) return; + + const chain = direction === "deposit" ? fromChain : toChain; + + if ( + // If the chain is an Cosmos chain, we don't need to connect the cosmos chain + chain.chainType !== "evm" || + // Or if the account is already connected + !!evmAddress || + // Or if there's no available evm chain + !firstSupportedEvmChain + ) { + return; + } + + onOpenBridgeWalletSelect(); + }, [ + direction, + evmAddress, + firstSupportedEvmChain, + fromChain, + onOpenBridgeWalletSelect, + toChain, ]); if ( @@ -546,81 +617,83 @@ export const AmountScreen = observer( {walletConnected && ( <> - + + { + const setChain = + direction === "deposit" ? setFromChain : setToChain; + setChain(chain); + resetAssets(); + }} + evmChain={ + sourceChain?.chainType === "evm" + ? sourceChain + : firstSupportedEvmChain + } + cosmosChain={ + sourceChain?.chainType === "cosmos" + ? sourceChain + : firstSupportedCosmosChain + } + /> + + ) : ( +
+ + {direction === "deposit" + ? t("transfer.transferWith") + : t("transfer.transferTo")} + +
- - - { - const setChain = - direction === "deposit" ? setFromChain : setToChain; - setChain(chain); - resetAssets(); - }} - evmChain={ - sourceChain?.chainType === "evm" - ? sourceChain - : supportedChainsAsBridgeChain.find( - ( - chain - ): chain is Extract< - BridgeChain, - { chainType: "evm" } - > => chain.chainType === "evm" - ) - } - cosmosChain={ - sourceChain?.chainType === "cosmos" - ? sourceChain - : supportedChainsAsBridgeChain.find( - ( - chain - ): chain is Extract< - BridgeChain, - { chainType: "cosmos" } - > => chain.chainType === "cosmos" - ) - } - /> + )} )} @@ -880,3 +953,17 @@ const AmountScreenSkeletonLoader = () => {
); }; + +const WalletDisplay: FunctionComponent<{ + icon: string | undefined; + name: string | undefined; + suffix?: ReactNode; +}> = ({ icon, name, suffix }) => { + return ( +
+ {!isNil(icon) && {name}} + {name} + {suffix} +
+ ); +}; From 27f767785e8492ac04df094baca80db949270b3b Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 27 Jun 2024 23:10:13 -0400 Subject: [PATCH 18/33] feat: set up wallet connection from immersive bridge --- .../bridge/immersive/amount-screen.tsx | 45 +++-- .../immersive/bridge-network-select.tsx | 174 ++++++++++++++---- .../bridge/immersive/bridge-wallet-select.tsx | 14 +- .../bridge/immersive/immersive-bridge.tsx | 30 --- .../wallet-states/switching-network-state.tsx | 40 ++++ 5 files changed, 220 insertions(+), 83 deletions(-) create mode 100644 packages/web/components/wallet-states/switching-network-state.tsx diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index cd2de10380..b2898e8722 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -2,7 +2,7 @@ import { Menu } from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; -import { MinimalAsset } from "@osmosis-labs/types"; +import { BridgeTransactionDirection, MinimalAsset } from "@osmosis-labs/types"; import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; @@ -54,8 +54,6 @@ export const AmountScreen = observer( ({ direction, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { const { accountStore } = useStore(); const { onOpenWalletSelect } = useWalletSelect(); - - const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); const { t } = useTranslation(); const { accountActionButton: connectWalletButton, walletConnected } = @@ -71,6 +69,8 @@ export const AmountScreen = observer( const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); + const [areMoreOptionsVisible, setAreMoreOptionsVisible] = useState(false); + const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); @@ -82,8 +82,11 @@ export const AmountScreen = observer( // Wallets const account = accountStore.getWallet(accountStore.osmosisChainId); - const { address: evmAddress, connector: evmConnector } = - useEvmWalletAccount(); + const { + address: evmAddress, + connector: evmConnector, + isConnected: isEvmWalletConnected, + } = useEvmWalletAccount(); const sourceChain = direction === "deposit" ? fromChain : toChain; const destinationChain = direction === "deposit" ? toChain : fromChain; @@ -293,7 +296,9 @@ export const AmountScreen = observer( // Or if the account is already connected !!cosmosCounterpartyAccount?.address || // Or if there's no available cosmos chain - !firstSupportedCosmosChain + !firstSupportedCosmosChain || + // Or if the account is already connected + !!cosmosCounterpartyAccountRepo?.current ) { return; } @@ -329,7 +334,7 @@ export const AmountScreen = observer( // If the chain is an Cosmos chain, we don't need to connect the cosmos chain chain.chainType !== "evm" || // Or if the account is already connected - !!evmAddress || + isEvmWalletConnected || // Or if there's no available evm chain !firstSupportedEvmChain ) { @@ -342,6 +347,7 @@ export const AmountScreen = observer( evmAddress, firstSupportedEvmChain, fromChain, + isEvmWalletConnected, onOpenBridgeWalletSelect, toChain, ]); @@ -460,6 +466,7 @@ export const AmountScreen = observer(
{ @@ -474,6 +481,7 @@ export const AmountScreen = observer( { @@ -633,12 +641,12 @@ export const AmountScreen = observer( icon={ sourceChain?.chainType === "evm" ? evmConnector?.icon - : account?.walletInfo.logo + : cosmosCounterpartyAccount?.walletInfo.logo } name={ sourceChain?.chainType === "evm" ? evmConnector?.name - : account?.walletInfo.prettyName + : cosmosCounterpartyAccount?.walletInfo.prettyName } suffix={ setIsMoreOptionsVisible(true)} + onClick={() => setAreMoreOptionsVisible(true)} disabled={isNil(sourceAsset) || isNil(destinationAsset)} > {direction === "deposit" @@ -869,7 +877,7 @@ export const AmountScreen = observer( setIsMoreOptionsVisible(false)} + onRequestClose={() => setAreMoreOptionsVisible(false)} /> )} @@ -892,12 +900,20 @@ export const AmountScreen = observer( ); const ChainSelectorButton: FunctionComponent<{ + direction: BridgeTransactionDirection; readonly: boolean; children: ReactNode; chainLogo: string; chains: ReturnType["supportedChains"]; onSelectChain: (chain: BridgeChain) => void; -}> = ({ readonly, children, chainLogo: _chainLogo, chains, onSelectChain }) => { +}> = ({ + direction, + readonly, + children, + chainLogo: _chainLogo, + chains, + onSelectChain, +}) => { const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); if (readonly) { @@ -928,11 +944,12 @@ const ChainSelectorButton: FunctionComponent<{ { + onSelectChain={async (chain) => { onSelectChain(chain); setIsNetworkSelectVisible(false); }} onRequestClose={() => setIsNetworkSelectVisible(false)} + direction={direction} /> )} diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index 9021b2615d..fb79ea2a8b 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -1,11 +1,27 @@ import type { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import classNames from "classnames"; import React, { useMemo, useState } from "react"; +import { BridgeWalletSelectScreen } from "~/components/bridge/immersive/bridge-wallet-select"; import { SearchBox } from "~/components/input"; +import { + Screen, + ScreenGoBackButton, + ScreenManager, +} from "~/components/screen-manager"; +import { SwitchingNetworkState } from "~/components/wallet-states/switching-network-state"; +import { EthereumChainIds } from "~/config/wagmi"; +import { useEvmWalletAccount, useSwitchEvmChain } from "~/hooks/evm-wallet"; import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; +enum Screens { + Main = "main", + SelectWallet = "select-wallet", +} interface BridgeNetworkSelectProps extends ModalBaseProps { + direction: BridgeTransactionDirection; chains: { prettyName: string; chainId: BridgeChain["chainId"]; @@ -15,12 +31,25 @@ interface BridgeNetworkSelectProps extends ModalBaseProps { } export const BridgeNetworkSelect = ({ + direction, chains, onSelectChain, ...modalProps }: BridgeNetworkSelectProps) => { const { t } = useTranslation(); + const [isSwitchingChain, setIsSwitchingChain] = useState(false); + + const [connectingToEvmChain, setConnectingToEvmChain] = + useState>(); + + const { + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + connector, + } = useEvmWalletAccount(); + const { switchChainAsync } = useSwitchEvmChain(); + const [query, setQuery] = useState(""); const filteredChains = useMemo(() => { @@ -30,39 +59,120 @@ export const BridgeNetworkSelect = ({ }, [chains, query]); return ( - { - setQuery(""); - }} - > - { - setQuery(nextValue); - }} - className="my-4 flex-shrink-0" - placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} - size="full" - /> -
- {filteredChains.map((chain) => ( - - ))} -
-
+ + { + setConnectingToEvmChain(undefined); + }} + /> + { + modalProps.onRequestClose(); + }} + direction={direction} + onSelectChain={(chain) => { + onSelectChain(chain); + }} + evmChain={connectingToEvmChain} + /> + + + + {isEvmWalletConnected && isSwitchingChain && ( +
+ +
+ )} + +
+ { + setQuery(nextValue); + }} + className="my-4 flex-shrink-0" + placeholder={t( + "transfer.bridgeNetworkSelect.searchPlaceholder" + )} + size="full" + /> +
+ {filteredChains.map((chain) => ( + + ))} +
+
+
+ + + )} + ); }; diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx index 054f423d6f..4cd1996889 100644 --- a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx @@ -80,9 +80,9 @@ export const BridgeWalletSelectScreen = ({ const { onConnect: onConnectWallet, wagmi: { - variables: wagmiVariables, - status: wagmiStatus, - error: wagmiError, + variables: connectingWagmiVariables, + status: connectingWagmiStatus, + error: connectingWagmiError, }, } = useConnectWallet({ walletOptions: [ @@ -108,14 +108,14 @@ export const BridgeWalletSelectScreen = ({ isMobile: false, }); - if (!isNil(wagmiVariables?.connector)) { + if (!isNil(connectingWagmiVariables?.connector)) { return (
diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 8d0ddeac8b..10b3d12c0f 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -12,13 +12,8 @@ import { StepProgress } from "~/components/stepper/progress-bar"; import { Button, IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; import { BridgeFlowProvider } from "~/hooks/bridge"; -import { - useDisconnectEvmWallet, - useEvmWalletAccount, -} from "~/hooks/evm-wallet"; import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; import { useDisclosure } from "~/hooks/use-disclosure"; -import { useWalletSelect } from "~/hooks/use-wallet-select"; import { FiatRampKey } from "~/integrations"; import { ModalCloseButton } from "~/modals"; import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; @@ -72,10 +67,6 @@ export const ImmersiveBridgeFlow = ({ onClose: onCloseFiatOnrampSelection, } = useDisclosure(); - const { isConnected, address } = useEvmWalletAccount(); - const { onOpenWalletSelect } = useWalletSelect(); - const { disconnect } = useDisconnectEvmWallet(); - useLockBodyScroll(isVisible); const onClose = () => { @@ -215,27 +206,6 @@ export const ImmersiveBridgeFlow = ({ )}
- {isConnected ? ( -
-

Evm Address: {address}

- -
- ) : ( - - )}
)} diff --git a/packages/web/components/wallet-states/switching-network-state.tsx b/packages/web/components/wallet-states/switching-network-state.tsx new file mode 100644 index 0000000000..46253a2578 --- /dev/null +++ b/packages/web/components/wallet-states/switching-network-state.tsx @@ -0,0 +1,40 @@ +import { isNil } from "@osmosis-labs/utils"; +import React from "react"; + +interface SwitchingNetworkStateProps { + walletLogo?: string; + title?: string; + desc?: string; + walletName?: string; +} + +export const SwitchingNetworkState = ({ + walletLogo, + title: titleProp, + desc: descProp, + walletName, +}: SwitchingNetworkStateProps) => { + const title = titleProp ?? "Switching network"; + const desc = + descProp ?? + `Open the ${walletName} browser extension to approve the network switch.`; + + return ( +
+
+ {!!walletLogo && typeof walletLogo === "string" && ( + Wallet logo + )} +
+ +
+ {!isNil(title) && ( +

{title}

+ )} + {!isNil(desc) && ( +

{desc}

+ )} +
+
+ ); +}; From 63570fa600afa9d376f0d94efc93e4ede6d87058 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 13:56:07 -0400 Subject: [PATCH 19/33] feat: create initial use bridge quote hook --- .../bridge/immersive/amount-screen.tsx | 4 +- .../immersive/bridge-network-select.tsx | 160 ++--- .../bridge/immersive/bridge-wallet-select.tsx | 4 +- .../bridge/immersive/use-bridge-quote.ts | 659 ++++++++++++++++++ 4 files changed, 745 insertions(+), 82 deletions(-) create mode 100644 packages/web/components/bridge/immersive/use-bridge-quote.ts diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index b2898e8722..c3d728a7a7 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -842,7 +842,7 @@ export const AmountScreen = observer( )} -
+ {/*
@@ -853,7 +853,7 @@ export const AmountScreen = observer( {t("transfer.calculatingFees")} -
+
*/}
{!walletConnected ? ( diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select.tsx index fb79ea2a8b..724bfd1c13 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select.tsx @@ -78,95 +78,99 @@ export const BridgeNetworkSelect = ({ }} > - { - setConnectingToEvmChain(undefined); - }} - /> - { - modalProps.onRequestClose(); - }} - direction={direction} - onSelectChain={(chain) => { - onSelectChain(chain); - }} - evmChain={connectingToEvmChain} - /> +
+ { + setConnectingToEvmChain(undefined); + }} + /> + { + modalProps.onRequestClose(); + }} + direction={direction} + onSelectChain={(chain) => { + onSelectChain(chain); + }} + evmChain={connectingToEvmChain} + /> +
- {isEvmWalletConnected && isSwitchingChain && ( -
- + {isEvmWalletConnected && isSwitchingChain && ( +
+ +
+ )} + +
+ { + setQuery(nextValue); + }} + className="my-4 flex-shrink-0" + placeholder={t( + "transfer.bridgeNetworkSelect.searchPlaceholder" + )} + size="full" /> -
- )} +
+ {filteredChains.map((chain) => ( + - ))} + } as BridgeChain); + }} + > + {chain.prettyName} + + ))} +
diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx index 4cd1996889..cbc8667afe 100644 --- a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-wallet-select.tsx @@ -253,14 +253,14 @@ const WalletButton: React.FC<{ return (
- - setIsNetworkSelectVisible(false)} /> diff --git a/packages/web/components/bridge/immersive/asset-select-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx index a5e49a7979..02bfebdb4c 100644 --- a/packages/web/components/bridge/immersive/asset-select-screen.tsx +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -24,10 +24,9 @@ import { UnverifiedAssetsState } from "~/stores/user-settings/unverified-assets" import { formatPretty } from "~/utils/formatter"; import { api, RouterOutputs } from "~/utils/trpc"; -const variantsNotToBeExcluded = ["WBTC", "BTC"] satisfies ( - | MainnetVariantGroupKeys - | TestnetVariantGroupKeys -)[]; +const variantsNotToBeExcluded = [ + "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", +] satisfies (MainnetVariantGroupKeys | TestnetVariantGroupKeys)[]; const prioritizedDenoms = [ "USDC", "OSMO", diff --git a/packages/web/components/bridge/immersive/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx similarity index 97% rename from packages/web/components/bridge/immersive/bridge-network-select.tsx rename to packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index 8601257bdd..f2f5a4e8c2 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; import { api } from "~/utils/trpc"; -export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { +export const BridgeNetworkSelectModal = (modalProps: ModalBaseProps) => { const { t } = useTranslation(); const [query, setQuery] = useState(""); diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 8c6a6144c3..a7f0df2aac 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -11,6 +11,7 @@ import { Screen, ScreenManager } from "~/components/screen-manager"; import { StepProgress } from "~/components/stepper/progress-bar"; import { Button, IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; +import { useTranslation } from "~/hooks"; import { BridgeFlowProvider } from "~/hooks/bridge"; import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; import { useDisclosure } from "~/hooks/use-disclosure"; @@ -20,7 +21,7 @@ import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; import { FiatRampsModal } from "~/modals/fiat-ramps"; import { api } from "~/utils/trpc"; -enum ImmersiveBridgeScreens { +const enum ImmersiveBridgeScreens { Asset = "0", Amount = "1", Review = "2", @@ -34,12 +35,13 @@ export const ImmersiveBridgeFlow = ({ Provider, children, }: PropsWithChildren) => { + const { t } = useTranslation(); + const [isVisible, setIsVisible] = useState(false); const [step, setStep] = useState( ImmersiveBridgeScreens.Asset ); - const [direction, setDirection] = - useState("deposit"); + const [direction, setDirection] = useState<"deposit" | "withdraw">("deposit"); const { logEvent } = useAmplitudeAnalytics(); const [selectedAssetDenom, setSelectedAssetDenom] = useState(); @@ -67,10 +69,6 @@ export const ImmersiveBridgeFlow = ({ onClose: onCloseFiatOnrampSelection, } = useDisclosure(); - // const { isConnected, address } = useEvmWalletAccount(); - // const { onOpenWalletSelect } = useWalletSelect(); - // const { disconnect } = useDisconnectEvmWallet(); - useLockBodyScroll(isVisible); const onClose = () => { @@ -159,21 +157,21 @@ export const ImmersiveBridgeFlow = ({ className="w-full" steps={[ { - displayLabel: "Asset", + displayLabel: t("transfer.stepLabels.asset"), onClick: step !== ImmersiveBridgeScreens.Asset ? () => setStep(ImmersiveBridgeScreens.Asset) : undefined, }, { - displayLabel: "Amount", + displayLabel: t("transfer.stepLabels.amount"), onClick: step === ImmersiveBridgeScreens.Review ? () => setStep(ImmersiveBridgeScreens.Amount) : undefined, }, { - displayLabel: "Review", + displayLabel: t("transfer.stepLabels.review"), }, ]} currentStep={Number(step)} @@ -192,12 +190,10 @@ export const ImmersiveBridgeFlow = ({ )} - {() => ( - - )} + {({ goBack }) => ( @@ -209,27 +205,6 @@ export const ImmersiveBridgeFlow = ({ )}
- {/* {isConnected ? ( -
-

Evm Address: {address}

- -
- ) : ( - - )} */}
)} diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 309f6e8f44..4769318f7a 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Weitere Auszahlungsoptionen", "convertTo": "Konvertieren zu", "recommended": "Empfohlen", + "stepLabels": { + "asset": "Vermögenswert", + "amount": "Menge", + "review": "Rezension" + }, "moreBridgeOptions": { "titleDeposit": "Mehr Einzahlungsmöglichkeiten", "titleWithdraw": "Weitere Auszahlungsoptionen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 08e903071d..2bb268972b 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "More withdraw options", "convertTo": "Convert to", "recommended": "Recommended", + "stepLabels": { + "asset": "Asset", + "amount": "Amount", + "review": "Review" + }, "moreBridgeOptions": { "titleDeposit": "More deposit options", "titleWithdraw": "More withdraw options", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index d6ff837080..bfc45bd820 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Más opciones de retiro", "convertTo": "Convertir a", "recommended": "Recomendado", + "stepLabels": { + "asset": "Activo", + "amount": "Cantidad", + "review": "Revisar" + }, "moreBridgeOptions": { "titleDeposit": "Más opciones de depósito", "titleWithdraw": "Más opciones de retiro", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 609a2a3d39..6745d7762c 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "گزینه های برداشت بیشتر", "convertTo": "تبدیل به", "recommended": "توصیه شده", + "stepLabels": { + "asset": "دارایی", + "amount": "میزان", + "review": "مرور" + }, "moreBridgeOptions": { "titleDeposit": "گزینه های سپرده بیشتر", "titleWithdraw": "گزینه های برداشت بیشتر", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index b26d8a5416..a7036fa597 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Plus d'options de retrait", "convertTo": "Convertir en", "recommended": "Recommandé", + "stepLabels": { + "asset": "Actif", + "amount": "Montant", + "review": "Revoir" + }, "moreBridgeOptions": { "titleDeposit": "Plus d'options de dépôt", "titleWithdraw": "Plus d'options de retrait", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index ac2b053206..0716e1ea57 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "વધુ ઉપાડ વિકલ્પો", "convertTo": "માં કન્વર્ટ કરો", "recommended": "ભલામણ કરેલ", + "stepLabels": { + "asset": "એસેટ", + "amount": "રકમ", + "review": "સમીક્ષા" + }, "moreBridgeOptions": { "titleDeposit": "વધુ થાપણ વિકલ્પો", "titleWithdraw": "વધુ ઉપાડ વિકલ્પો", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 6422f07654..62866cb9d5 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "अधिक निकासी विकल्प", "convertTo": "में बदलो", "recommended": "अनुशंसित", + "stepLabels": { + "asset": "संपत्ति", + "amount": "मात्रा", + "review": "समीक्षा" + }, "moreBridgeOptions": { "titleDeposit": "अधिक जमा विकल्प", "titleWithdraw": "अधिक निकासी विकल्प", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 683552416f..084accf75a 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "引き出しオプションの追加", "convertTo": "に変換", "recommended": "推奨", + "stepLabels": { + "asset": "資産", + "amount": "額", + "review": "レビュー" + }, "moreBridgeOptions": { "titleDeposit": "その他の入金オプション", "titleWithdraw": "引き出しオプションの追加", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 9475f50ea0..02d79d0ded 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "더 많은 인출 옵션", "convertTo": "로 변환하다", "recommended": "추천", + "stepLabels": { + "asset": "유산", + "amount": "양", + "review": "검토" + }, "moreBridgeOptions": { "titleDeposit": "더 많은 입금 옵션", "titleWithdraw": "더 많은 인출 옵션", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index daad7fd6e6..a5c3a67734 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Więcej opcji wypłat", "convertTo": "Konwertuj na", "recommended": "Zalecana", + "stepLabels": { + "asset": "Zaleta", + "amount": "Kwota", + "review": "Recenzja" + }, "moreBridgeOptions": { "titleDeposit": "Więcej opcji depozytu", "titleWithdraw": "Więcej opcji wypłat", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 72272521f8..105c6ce634 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Mais opções de retirada", "convertTo": "Converter para", "recommended": "Recomendado", + "stepLabels": { + "asset": "Ativo", + "amount": "Quantia", + "review": "Análise" + }, "moreBridgeOptions": { "titleDeposit": "Mais opções de depósito", "titleWithdraw": "Mais opções de retirada", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 282fb29690..ef2569777f 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Mai multe opțiuni de retragere", "convertTo": "Schimba in", "recommended": "Recomandat", + "stepLabels": { + "asset": "Atu", + "amount": "Cantitate", + "review": "Revizuire" + }, "moreBridgeOptions": { "titleDeposit": "Mai multe opțiuni de depunere", "titleWithdraw": "Mai multe opțiuni de retragere", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 8940ed6a5d..8eab4fa7f4 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Больше вариантов вывода", "convertTo": "Перевести в", "recommended": "рекомендуемые", + "stepLabels": { + "asset": "Объект", + "amount": "Количество", + "review": "Обзор" + }, "moreBridgeOptions": { "titleDeposit": "Больше вариантов депозита", "titleWithdraw": "Больше вариантов вывода", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 2011520592..e4ef37e8bc 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Daha fazla para çekme seçeneği", "convertTo": "E dönüşmek", "recommended": "Tavsiye edilen", + "stepLabels": { + "asset": "Varlık", + "amount": "Miktar", + "review": "Gözden geçirmek" + }, "moreBridgeOptions": { "titleDeposit": "Daha fazla para yatırma seçeneği", "titleWithdraw": "Daha fazla para çekme seçeneği", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index d0ebb0dfa0..91a72f522f 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款选项", "convertTo": "转换成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "资产", + "amount": "数量", + "review": "审查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款选择", "titleWithdraw": "更多提款选项", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 777b2dfbc7..e820dfdc7b 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "資產", + "amount": "數量", + "review": "審查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款選擇", "titleWithdraw": "更多提款選項", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 8ae1bb96d0..660baf480f 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "資產", + "amount": "數量", + "review": "審查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款選擇", "titleWithdraw": "更多提款選項", From 2868ebaf7a439f313520a5bb3c627a82e324908f Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 14:45:03 -0400 Subject: [PATCH 21/33] feat: remove source denom from minimal asset --- packages/types/src/asset-types.ts | 3 +-- packages/utils/src/asset-utils.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/types/src/asset-types.ts b/packages/types/src/asset-types.ts index 920f57ab3a..a555925327 100644 --- a/packages/types/src/asset-types.ts +++ b/packages/types/src/asset-types.ts @@ -180,7 +180,7 @@ export interface Asset { variantGroupKey?: string; } -export type MinimalAsset = KeplrBaseCurrency & { +export type MinimalAsset = { coinDenom: string; coinMinimalDenom: string; coinDecimals: number; @@ -193,6 +193,5 @@ export type MinimalAsset = KeplrBaseCurrency & { coinName: string; isUnstable: boolean; isVerified: boolean; - sourceDenom: string; variantGroupKey: string | undefined; }; diff --git a/packages/utils/src/asset-utils.ts b/packages/utils/src/asset-utils.ts index 8b6ccaeac4..9bfa8428f0 100644 --- a/packages/utils/src/asset-utils.ts +++ b/packages/utils/src/asset-utils.ts @@ -60,7 +60,6 @@ export function makeMinimalAsset(assetListAsset: Asset): MinimalAsset { name, unstable, verified, - sourceDenom, variantGroupKey, } = assetListAsset; @@ -70,7 +69,6 @@ export function makeMinimalAsset(assetListAsset: Asset): MinimalAsset { coinMinimalDenom, coinDecimals: decimals, coinGeckoId: coingeckoId, - sourceDenom, coinImageUrl: relative_image_url, isUnstable: unstable, isVerified: verified, From 8be9aa4fc1acf8816eeec0c7c73814e361c4f3b9 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 14:48:08 -0400 Subject: [PATCH 22/33] test: update snapshots --- .../src/queries/complex/assets/__tests__/assets.spec.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts index d0141fd9df..90ea9deea3 100644 --- a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts +++ b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts @@ -104,7 +104,6 @@ describe("getAssetWithVariants", () => { "coinName": "USDC", "isUnstable": false, "isVerified": true, - "sourceDenom": "uusdc", "variantGroupKey": "USDC", }, { @@ -116,7 +115,6 @@ describe("getAssetWithVariants", () => { "coinName": "USD Coin (Axelar)", "isUnstable": false, "isVerified": true, - "sourceDenom": "uusdc", "variantGroupKey": "USDC", }, { @@ -128,7 +126,6 @@ describe("getAssetWithVariants", () => { "coinName": "USD Coin (Polygon)", "isUnstable": false, "isVerified": true, - "sourceDenom": "polygon-uusdc", "variantGroupKey": "USDC", }, { @@ -140,7 +137,6 @@ describe("getAssetWithVariants", () => { "coinName": "USD Coin (Avalanche)", "isUnstable": false, "isVerified": true, - "sourceDenom": "avalanche-uusdc", "variantGroupKey": "USDC", }, { @@ -152,7 +148,6 @@ describe("getAssetWithVariants", () => { "coinName": "USDC (Gravity Bridge)", "isUnstable": false, "isVerified": true, - "sourceDenom": "gravity0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "variantGroupKey": "USDC", }, { @@ -164,7 +159,6 @@ describe("getAssetWithVariants", () => { "coinName": "USD Coin (Wormhole)", "isUnstable": false, "isVerified": true, - "sourceDenom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt", "variantGroupKey": "USDC", }, { @@ -176,7 +170,6 @@ describe("getAssetWithVariants", () => { "coinName": "Solana USD Coin (Wormhole)", "isUnstable": false, "isVerified": true, - "sourceDenom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/HJk1XMDRNUbRrpKkNZYui7SwWDMjXZAsySzqgyNcQoU3", "variantGroupKey": "USDC", }, ] @@ -209,7 +202,6 @@ describe("getAssetWithVariants", () => { "coinName": "Cosmos Hub", "isUnstable": false, "isVerified": true, - "sourceDenom": "uatom", "variantGroupKey": "ATOM", }, ] From 8981144d31744b13f102b4f9c0f77bf11b8aa559 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 22:05:36 -0400 Subject: [PATCH 23/33] feat: receive quote from input --- packages/bridge/package.json | 2 +- packages/server/package.json | 2 +- .../server/src/queries/complex/assets/user.ts | 12 +- packages/utils/package.json | 2 +- .../bridge/immersive/amount-screen.tsx | 72 +++- .../immersive/bridge-network-select-modal.tsx | 239 ++++++++---- ...ect.tsx => bridge-wallet-select-modal.tsx} | 50 +-- .../bridge/immersive/immersive-bridge.tsx | 1 + .../bridge/immersive/use-bridge-quote.ts | 361 +++++++++--------- .../immersive/use-bridges-supported-assets.ts | 1 - packages/web/hooks/evm-wallet.ts | 9 +- packages/web/hooks/use-feature-flags.ts | 1 - packages/web/package.json | 4 +- .../web/server/api/routers/bridge-transfer.ts | 4 +- .../api/routers/local-bridge-transfer.ts | 63 ++- packages/web/utils/ethereum.ts | 40 ++ yarn.lock | 107 +++--- 17 files changed, 593 insertions(+), 377 deletions(-) rename packages/web/components/bridge/immersive/{bridge-wallet-select.tsx => bridge-wallet-select-modal.tsx} (91%) create mode 100644 packages/web/utils/ethereum.ts diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 4173bc14e3..443266043e 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -38,7 +38,7 @@ "cachified": "^3.5.4", "long": "^5.2.3", "lru-cache": "^10.0.1", - "viem": "2.13.3", + "viem": "2.16.4", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/server/package.json b/packages/server/package.json index 961bf76d4a..2492fc2c75 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -46,7 +46,7 @@ "lru-cache": "^10.0.1", "superjson": "^2.2.1", "zod": "^3.22.4", - "viem": "2.13.3" + "viem": "2.16.4" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/server/src/queries/complex/assets/user.ts b/packages/server/src/queries/complex/assets/user.ts index 8c8c3221a6..1c0f54547b 100644 --- a/packages/server/src/queries/complex/assets/user.ts +++ b/packages/server/src/queries/complex/assets/user.ts @@ -32,13 +32,11 @@ export async function getAssetWithUserBalance({ chainList, asset, userCosmosAddress, - chainId, }: { assetLists: AssetList[]; chainList: Chain[]; asset: TAsset; userCosmosAddress?: string; - chainId?: string; }): Promise { if (!userCosmosAddress) return asset; @@ -48,7 +46,6 @@ export async function getAssetWithUserBalance({ assets: [asset], userCosmosAddress: userCosmosAddress, includePreview: true, - chainId, }); return userAssets[0]; } @@ -60,13 +57,11 @@ export async function mapGetAssetsWithUserBalances< TAsset extends MinimalAsset >({ poolId, - chainId, ...params }: { assetLists: AssetList[]; chainList: Chain[]; assets?: TAsset[]; - chainId?: string; userCosmosAddress?: string; sortFiatValueDirection?: SortDirection; /** @@ -96,17 +91,12 @@ export async function mapGetAssetsWithUserBalances< const { balances } = await queryBalances({ ...params, - // Defaults to Osmosis - chainId, bech32Address: userCosmosAddress, }); const eventualUserAssets = assets .map(async (asset) => { - const balance = balances.find( - (a) => - a.denom === asset.coinMinimalDenom || a.denom === asset.sourceDenom // If it's outside of Osmosis - ); + const balance = balances.find((a) => a.denom === asset.coinMinimalDenom); // not a user asset if (!balance) return asset; diff --git a/packages/utils/package.json b/packages/utils/package.json index f6d2905515..f8f3cc0b07 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -17,7 +17,7 @@ "@keplr-wallet/unit": "0.10.24-ibc.go.v7.hot.fix", "@osmosis-labs/types": "^1.0.0", "sha.js": "^2.4.11", - "viem": "2.13.3" + "viem": "2.16.4" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 78f294180b..10ab9ec149 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -17,8 +17,9 @@ import { import { Icon } from "~/components/assets"; import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; -import { BridgeWalletSelect } from "~/components/bridge/immersive/bridge-wallet-select"; +import { BridgeWalletSelectModal } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; +import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; @@ -48,10 +49,17 @@ interface AmountScreenProps { * Includes both the canonical asset and its variants. */ assetsInOsmosis: MinimalAsset[] | undefined; + + onClose: () => void; } export const AmountScreen = observer( - ({ direction, assetsInOsmosis, selectedDenom }: AmountScreenProps) => { + ({ + direction, + assetsInOsmosis, + selectedDenom, + onClose, + }: AmountScreenProps) => { const { accountStore } = useStore(); const { onOpenWalletSelect } = useWalletSelect(); const { t } = useTranslation(); @@ -64,7 +72,9 @@ export const AmountScreen = observer( noop ); - const [sourceAsset, setSourceAsset] = useState(); + const [sourceAsset, setSourceAsset] = useState< + SupportedAsset & { amount: CoinPretty } + >(); const [destinationAsset, setDestinationAsset] = useState(); const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); @@ -81,7 +91,9 @@ export const AmountScreen = observer( } = useDisclosure(); // Wallets - const account = accountStore.getWallet(accountStore.osmosisChainId); + const destinationAccount = accountStore.getWallet( + accountStore.osmosisChainId + ); const { address: evmAddress, connector: evmConnector, @@ -100,7 +112,7 @@ export const AmountScreen = observer( ? undefined : accountStore.getWallet(sourceChain.chainId); - const currentAddress = + const sourceAddress = sourceChain?.chainType === "evm" ? evmAddress : cosmosCounterpartyAccount?.address; @@ -303,16 +315,18 @@ export const AmountScreen = observer( return; } - cosmosCounterpartyAccountRepo?.connect(account?.walletName).catch(() => - // Display the connect modal if the user for some reason rejects the connection - onOpenWalletSelect({ - walletOptions: [ - { walletType: "cosmos", chainId: String(chain.chainId) }, - ], - }) - ); + cosmosCounterpartyAccountRepo + ?.connect(destinationAccount?.walletName) + .catch(() => + // Display the connect modal if the user for some reason rejects the connection + onOpenWalletSelect({ + walletOptions: [ + { walletType: "cosmos", chainId: String(chain.chainId) }, + ], + }) + ); }, [ - account?.walletName, + destinationAccount?.walletName, cosmosCounterpartyAccount?.address, cosmosCounterpartyAccountRepo, direction, @@ -352,6 +366,27 @@ export const AmountScreen = observer( toChain, ]); + const { bridgeProviders, selectedQuote } = useBridgeQuote({ + destinationAddress: destinationAccount?.address, + destinationChain, + destinationAsset: destinationAsset + ? { + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + denom: destinationAsset.coinDenom, + } + : undefined, + sourceAddress, + sourceChain, + sourceAsset, + direction, + onRequestClose: onClose, + inputAmount: cryptoAmount, + bridges: sourceAsset?.supportedProviders, + }); + + console.log(bridgeProviders, selectedQuote); + if ( isLoadingCanonicalAssetPrice || isNil(supportedAssets) || @@ -659,7 +694,7 @@ export const AmountScreen = observer( /> -
@@ -883,11 +918,10 @@ export const AmountScreen = observer( address: destinationAsset.coinMinimalDenom, decimals: destinationAsset.coinDecimals, denom: destinationAsset.coinDenom, - sourceDenom: destinationAsset.sourceDenom, }} fromChain={fromChain} toChain={toChain} - toAddress={currentAddress} + toAddress={sourceAddress} onRequestClose={() => setAreMoreOptionsVisible(false)} /> diff --git a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index f2f5a4e8c2..76967e323e 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -1,89 +1,182 @@ -import { debounce } from "debounce"; +import type { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import classNames from "classnames"; import React, { useMemo, useState } from "react"; +import { BridgeWalletSelectScreen } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { SearchBox } from "~/components/input"; -import { Intersection } from "~/components/intersection"; -import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { + Screen, + ScreenGoBackButton, + ScreenManager, +} from "~/components/screen-manager"; +import { SwitchingNetworkState } from "~/components/wallet-states/switching-network-state"; +import { EthereumChainIds } from "~/config/wagmi"; +import { useEvmWalletAccount, useSwitchEvmChain } from "~/hooks/evm-wallet"; import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; -import { api } from "~/utils/trpc"; -export const BridgeNetworkSelectModal = (modalProps: ModalBaseProps) => { +enum Screens { + Main = "main", + SelectWallet = "select-wallet", +} +interface BridgeNetworkSelectModalProps extends ModalBaseProps { + direction: BridgeTransactionDirection; + chains: { + prettyName: string; + chainId: BridgeChain["chainId"]; + chainType: BridgeChain["chainType"]; + }[]; + onSelectChain: (chain: BridgeChain) => void; +} + +export const BridgeNetworkSelectModal = ({ + direction, + chains, + onSelectChain, + ...modalProps +}: BridgeNetworkSelectModalProps) => { const { t } = useTranslation(); + const [isSwitchingChain, setIsSwitchingChain] = useState(false); + + const [connectingToEvmChain, setConnectingToEvmChain] = + useState>(); + + const { + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + connector, + } = useEvmWalletAccount(); + const { switchChainAsync } = useSwitchEvmChain(); + const [query, setQuery] = useState(""); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - api.edge.chains.getChains.useInfiniteQuery( - { - limit: 50, - search: query, - }, - { - enabled: modalProps.isOpen, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialCursor: 0, - keepPreviousData: true, - trpc: { - context: { - skipBatch: true, - }, - }, - } + const filteredChains = useMemo(() => { + return chains.filter(({ prettyName }) => + prettyName.toLowerCase().includes(query.toLowerCase()) ); - - const chains = useMemo( - () => data?.pages.flatMap((page) => page?.items) ?? [], - [data] - ); - const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; + }, [chains, query]); return ( - - { - setQuery(nextValue); - }, 300)} - className="my-4 flex-shrink-0" - placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} - size="full" - /> -
- {isLoading ? ( - <> - {new Array(3).fill(undefined).map((_, i) => ( - - ))} - - ) : ( - <> - {chains.map(({ chain_name, pretty_name }) => ( - - ))} - { - if (canLoadMore) { - fetchNextPage(); - } - }} - /> - {isFetchingNextPage && ( -
- + + {({ currentScreen, setCurrentScreen }) => ( + <> + { + setQuery(""); + setCurrentScreen(Screens.Main); + }} + > + +
+ { + setConnectingToEvmChain(undefined); + }} + /> + { + modalProps.onRequestClose(); + }} + direction={direction} + onSelectChain={(chain) => { + onSelectChain(chain); + }} + evmChain={connectingToEvmChain} + /> +
+
+ + +
+ {isEvmWalletConnected && isSwitchingChain && ( +
+ +
+ )} + +
+ { + setQuery(nextValue); + }} + className="my-4 flex-shrink-0" + placeholder={t( + "transfer.bridgeNetworkSelect.searchPlaceholder" + )} + size="full" + /> +
+ {filteredChains.map((chain) => ( + + ))} +
+
- )} - - )} -
- + + + + )} + ); }; diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx similarity index 91% rename from packages/web/components/bridge/immersive/bridge-wallet-select.tsx rename to packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx index cbc8667afe..fa9856d5e8 100644 --- a/packages/web/components/bridge/immersive/bridge-wallet-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx @@ -13,7 +13,6 @@ import { useDisconnectEvmWallet, useEvmWalletAccount, } from "~/hooks/evm-wallet"; -import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; @@ -27,30 +26,32 @@ interface BridgeWalletSelectProps extends ModalBaseProps { onSelectChain: (chain: BridgeChain) => void; } -export const BridgeWalletSelect = observer((props: BridgeWalletSelectProps) => { - const { direction, cosmosChain, evmChain, onSelectChain, ...modalProps } = - props; +export const BridgeWalletSelectModal = observer( + (props: BridgeWalletSelectProps) => { + const { direction, cosmosChain, evmChain, onSelectChain, ...modalProps } = + props; - return ( - - - - ); -}); + return ( + + + + ); + } +); export const BridgeWalletSelectScreen = ({ cosmosChain, @@ -63,7 +64,6 @@ export const BridgeWalletSelectScreen = ({ > & { onClose: () => void; }) => { - const { t } = useTranslation(); const { accountStore } = useStore(); const cosmosAccount = cosmosChain ? accountStore.getWallet(cosmosChain.chainId) diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index 27bc65a062..b2e594d113 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -195,6 +195,7 @@ export const ImmersiveBridgeFlow = ({ direction={direction} assetsInOsmosis={canonicalAssetsWithVariants} selectedDenom={selectedAssetDenom!} + onClose={() => setIsVisible(false)} /> )} diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index 6b5ddb6251..979c8e6553 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -3,15 +3,25 @@ import { Bridge, BridgeAsset, BridgeChain, + BridgeError, + CosmosBridgeTransactionRequest, + EvmBridgeTransactionRequest, GetTransferStatusParams, } from "@osmosis-labs/bridge"; -import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import { DeliverTxResponse } from "@osmosis-labs/stores"; +import { isNil } from "@osmosis-labs/utils"; import dayjs from "dayjs"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDebounce, useUnmount } from "react-use"; +import { Address } from "viem"; +import { BaseError } from "wagmi"; +import { displayToast } from "~/components/alert/toast"; +import { ToastType } from "~/components/alert/types"; +import { useEvmWalletAccount, useSendEvmTransaction } from "~/hooks/evm-wallet"; import { useTranslation } from "~/hooks/language"; import { useStore } from "~/stores"; +import { getWagmiToastErrorMessage } from "~/utils/ethereum"; import { api, RouterInputs } from "~/utils/trpc"; export const useBridgeQuote = ({ @@ -28,22 +38,34 @@ export const useBridgeQuote = ({ destinationChain, bridges = ["Axelar", "Skip", "Squid", "IBC"], + + onRequestClose, }: { - direction: BridgeTransactionDirection; + direction: "deposit" | "withdraw"; inputAmount: string; - sourceAsset: BridgeAsset & { amount: CoinPretty }; - sourceChain: BridgeChain; - sourceAddress: string; + sourceAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; + sourceChain: BridgeChain | undefined; + sourceAddress: string | undefined; - destinationAsset: BridgeAsset; - destinationChain: BridgeChain; - destinationAddress: string; + destinationAsset: BridgeAsset | undefined; + destinationChain: BridgeChain | undefined; + destinationAddress: string | undefined; bridges?: Bridge[]; + + onRequestClose: () => void; }) => { - const { transferHistoryStore } = useStore(); + const { accountStore, transferHistoryStore, queriesStore } = useStore(); + const { + connector: evmConnector, + address: evmAddress, + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + } = useEvmWalletAccount(); + const { sendTransactionAsync, isLoading: isEthTxPending } = + useSendEvmTransaction(); const { t } = useTranslation(); // In the context of Osmosis, this refers to the Osmosis chain. @@ -53,7 +75,7 @@ export const useBridgeQuote = ({ chain: destinationChain, }; - const counterpartyPath = { + const sourcePath = { address: sourceAddress, asset: sourceAsset, chain: sourceChain, @@ -62,16 +84,18 @@ export const useBridgeQuote = ({ const isDeposit = direction === "deposit"; const isWithdraw = direction === "withdraw"; - const quoteParams: Omit< - RouterInputs["bridgeTransfer"]["getQuoteByBridge"], - "bridge" | "fromAmount" + const quoteParams: Partial< + Omit< + RouterInputs["bridgeTransfer"]["getQuoteByBridge"], + "bridge" | "fromAmount" + > > = { - fromAddress: isDeposit ? counterpartyPath.address : destinationPath.address, - fromAsset: isDeposit ? counterpartyPath.asset : destinationPath.asset, - fromChain: isDeposit ? counterpartyPath.chain : destinationPath.chain, - toAddress: isDeposit ? destinationPath.address : counterpartyPath.address, - toAsset: isDeposit ? destinationPath.asset : counterpartyPath.asset, - toChain: isDeposit ? destinationPath.chain : counterpartyPath.chain, + fromAddress: isDeposit ? sourcePath.address : destinationPath.address, + fromAsset: isDeposit ? sourcePath.asset : destinationPath.asset, + fromChain: isDeposit ? sourcePath.chain : destinationPath.chain, + toAddress: isDeposit ? destinationPath.address : sourcePath.address, + toAsset: isDeposit ? destinationPath.asset : sourcePath.asset, + toChain: isDeposit ? destinationPath.chain : sourcePath.chain, }; const [selectedBridgeProvider, setSelectedBridgeProvider] = @@ -96,20 +120,21 @@ export const useBridgeQuote = ({ debouncedInputValue === "" ? "0" : debouncedInputValue ).mul( // CoinPretty only accepts whole amounts - DecUtils.getTenExponentNInPrecisionRange(destinationAsset.decimals) + DecUtils.getTenExponentNInPrecisionRange(destinationAsset?.decimals ?? 0) ); const quoteResults = api.useQueries((t) => bridges.map((bridge) => t.bridgeTransfer.getQuoteByBridge( { - ...quoteParams, + ...(quoteParams as Required), bridge, fromAmount: inputAmount.truncate().toString(), }, { enabled: - inputAmount.gt(new Dec(0)) && counterpartyPath.address !== "", + inputAmount.gt(new Dec(0)) && + Object.values(quoteParams).every((param) => !isNil(param)), staleTime: 5_000, cacheTime: 5_000, // Disable retries, as useQueries @@ -160,7 +185,7 @@ export const useBridgeQuote = ({ { coinDecimals: estimatedGasFee.decimals, coinDenom: estimatedGasFee.denom, - coinMinimalDenom: estimatedGasFee.sourceDenom, + coinMinimalDenom: estimatedGasFee.address, }, new Dec(estimatedGasFee.amount) ).maxDecimals(8) @@ -170,7 +195,7 @@ export const useBridgeQuote = ({ { coinDecimals: transferFee.decimals, coinDenom: transferFee.denom, - coinMinimalDenom: transferFee.sourceDenom, + coinMinimalDenom: transferFee.address, }, new Dec(transferFee.amount) ).maxDecimals(8), @@ -179,7 +204,7 @@ export const useBridgeQuote = ({ { coinDecimals: expectedOutput.decimals, coinDenom: expectedOutput.denom, - coinMinimalDenom: expectedOutput.sourceDenom, + coinMinimalDenom: expectedOutput.address, }, new Dec(expectedOutput.amount) ), @@ -220,6 +245,19 @@ export const useBridgeQuote = ({ )?.data; }, [quoteResults, selectedBridgeProvider]); + const bridgeProviders = useMemo( + () => + quoteResults + .map(({ data }) => data?.provider) + ?.filter((r): r is NonNullable => !!r) + .map(({ id, logoUrl }) => ({ + id: id, + logo: logoUrl, + name: id, + })), + [quoteResults] + ); + const numSucceeded = quoteResults.filter(({ isSuccess }) => isSuccess).length; const isOneSuccessful = Boolean(numSucceeded); const amountOfErrors = quoteResults.filter(({ isError }) => isError).length; @@ -286,10 +324,11 @@ export const useBridgeQuote = ({ isBridgeProviderControlledMode, ]); - const availableBalance = sourceAsset.amount; + const availableBalance = sourceAsset?.amount; const isInsufficientFee = inputAmountRaw !== "" && + availableBalance && selectedQuote?.transferFee !== undefined && selectedQuote?.transferFee.denom === availableBalance.denom && // make sure the fee is in the same denom as the asset new CoinPretty(availableBalance.currency, inputAmount) @@ -308,7 +347,7 @@ export const useBridgeQuote = ({ const bridgeTransaction = api.bridgeTransfer.getTransactionRequestByBridge.useQuery( { - ...quoteParams, + ...(quoteParams as Required), fromAmount: inputAmount.truncate().toString(), bridge: selectedBridgeProvider!, }, @@ -322,7 +361,8 @@ export const useBridgeQuote = ({ !selectedQuote?.transactionRequest && inputAmount.gt(new Dec(0)) && !isInsufficientBal && - !isInsufficientFee, + !isInsufficientFee && + Object.values(quoteParams).every((param) => !isNil(param)), refetchInterval: 30 * 1000, // 30 seconds } ); @@ -335,7 +375,7 @@ export const useBridgeQuote = ({ const [transferInitiated, setTransferInitiated] = useState(false); const trackTransferStatus = useCallback( (providerId: Bridge, params: GetTransferStatusParams) => { - if (inputAmountRaw !== "") { + if (inputAmountRaw !== "" && availableBalance) { transferHistoryStore.pushTxNow( `${providerId}${JSON.stringify(params)}`, new CoinPretty(availableBalance.currency, inputAmount) @@ -347,37 +387,37 @@ export const useBridgeQuote = ({ } }, [ - availableBalance.currency, + availableBalance, destinationAddress, inputAmount, inputAmountRaw, isWithdraw, - nonIbcBridgeHistoryStore, + transferHistoryStore, ] ); const [isApprovingToken, setIsApprovingToken] = useState(false); + const isSendTxPending = (() => { + if (!destinationChain) return false; + return destinationChain.chainType === "cosmos" + ? accountStore.getWallet(destinationChain.chainId)?.txTypeInProgress !== + "" + : isEthTxPending; + })(); + // close modal when initial eth transaction is committed - const isSendTxPending = isWithdraw - ? osmosisAccount?.txTypeInProgress !== "" - : isEthTxPending; useEffect(() => { if (transferInitiated && !isSendTxPending) { onRequestClose(); } - }, [ - transferInitiated, - isSendTxPending, - osmosisAccount?.txTypeInProgress, - isEthTxPending, - onRequestClose, - ]); + }, [isSendTxPending, onRequestClose, transferInitiated]); const handleEvmTx = async ( quote: NonNullable["quote"] ) => { - if (!ethWalletClient) throw new Error("No ETH wallet client found"); + if (!isEvmWalletConnected || !evmAddress || !evmConnector) + throw new Error("No ETH wallet account is connected"); const transactionRequest = quote.transactionRequest as EvmBridgeTransactionRequest; @@ -388,42 +428,13 @@ export const useBridgeQuote = ({ if (transactionRequest.approvalTransactionRequest) { setIsApprovingToken(true); - await ethWalletClient.send({ - method: "eth_sendTransaction", - params: [ - { - to: transactionRequest.approvalTransactionRequest.to, - from: ethWalletClient.accountAddress, - data: transactionRequest.approvalTransactionRequest.data, - }, - ], + await sendTransactionAsync({ + to: transactionRequest.approvalTransactionRequest.to as Address, + account: evmAddress, + data: transactionRequest.approvalTransactionRequest.data as Address, }); - await new Promise((resolve, reject) => { - const onConfirmed = () => { - setIsApprovingToken(false); - clearEvents(); - resolve(void 0); - }; - const onFailed = () => { - setIsApprovingToken(false); - clearEvents(); - reject(void 0); - }; - - const clearEvents = () => { - ethWalletClient.txStatusEventEmitter!.removeListener( - "confirmed", - onConfirmed - ); - ethWalletClient.txStatusEventEmitter!.removeListener( - "failed", - onFailed - ); - }; - ethWalletClient.txStatusEventEmitter!.on("confirmed", onConfirmed); - ethWalletClient.txStatusEventEmitter!.on("failed", onFailed); - }); + setIsApprovingToken(false); for (const quoteResult of quoteResults) { await quoteResult.refetch(); @@ -432,88 +443,62 @@ export const useBridgeQuote = ({ return; } - const txHash = await ethWalletClient.send({ - method: "eth_sendTransaction", - params: [ - { - to: transactionRequest.to, - from: ethWalletClient.accountAddress, - value: transactionRequest?.value - ? transactionRequest.value - : undefined, - data: transactionRequest.data, - gas: transactionRequest.gas, - gasPrice: transactionRequest.gasPrice, - maxFeePerGas: transactionRequest.maxFeePerGas, - maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, - }, - ], + const txHash = await sendTransactionAsync({ + to: transactionRequest.to, + account: evmAddress, + value: transactionRequest?.value + ? BigInt(transactionRequest.value) + : undefined, + data: transactionRequest.data, + gas: transactionRequest.gas + ? BigInt(transactionRequest.gas) + : undefined, + gasPrice: transactionRequest.gasPrice + ? BigInt(transactionRequest.gasPrice) + : undefined, + maxFeePerGas: transactionRequest.maxFeePerGas + ? BigInt(transactionRequest.maxFeePerGas) + : undefined, + maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas + ? BigInt(transactionRequest.maxPriorityFeePerGas) + : undefined, }); - await new Promise((resolve, reject) => { - const onConfirm = () => { - trackTransferStatus(quote.provider.id, { - sendTxHash: txHash as string, - fromChainId: quote.fromChain.chainId, - toChainId: quote.toChain.chainId, - }); - setLastDepositAccountEvmAddress(ethWalletClient.accountAddress!); + trackTransferStatus(quote.provider.id, { + sendTxHash: txHash as string, + fromChainId: quote.fromChain.chainId, + toChainId: quote.toChain.chainId, + }); - if (isWithdraw) { - withdrawAmountConfig.setAmount(""); - } else { - setDepositAmount(""); - } - setTransferInitiated(true); + // setLastDepositAccountEvmAddress(ethWalletClient.accountAddress!); - clearEvents(); - resolve(void 0); - }; - - const onFailed = () => { - clearEvents(); - reject(void 0); - }; - - const clearEvents = () => { - ethWalletClient.txStatusEventEmitter!.removeListener( - "confirmed", - onConfirm - ); - ethWalletClient.txStatusEventEmitter!.removeListener( - "failed", - onFailed - ); - }; - - ethWalletClient.txStatusEventEmitter!.on("confirmed", onConfirm); - ethWalletClient.txStatusEventEmitter!.on("failed", onFailed); - }); + // if (isWithdraw) { + // withdrawAmountConfig.setAmount(""); + // } else { + // setDepositAmount(""); + // } + setTransferInitiated(true); } catch (e) { - const msg = ethWalletClient.displayError?.(e); - if (typeof msg === "string") { - displayToast( - { - titleTranslationKey: "transactionFailed", - captionTranslationKey: msg, - }, - ToastType.ERROR - ); - } else if (msg) { - displayToast(msg, ToastType.ERROR); - } else { - console.error(e); - } + const error = e as BaseError; + const toastContent = getWagmiToastErrorMessage({ + error, + t, + walletName: evmConnector.name, + }); + displayToast(toastContent, ToastType.ERROR); } }; const handleCosmosTx = async ( quote: NonNullable["quote"] ) => { + if (!destinationChain || destinationChain?.chainType !== "cosmos") { + throw new Error("Destination chain is not cosmos"); + } const transactionRequest = quote.transactionRequest as CosmosBridgeTransactionRequest; return accountStore.signAndBroadcast( - osmosisChainId, // Osmosis chain id. For now all Cosmos transactions will come from Osmosis + destinationChain.chainId, transactionRequest.msgTypeUrl, [ { @@ -526,15 +511,16 @@ export const useBridgeQuote = ({ undefined, (tx: DeliverTxResponse) => { if (tx.code == null || tx.code === 0) { - const queries = queriesStore.get(osmosisChainId); + const queries = queriesStore.get(destinationChain.chainId); // After succeeding to send token, refresh the balance. const queryBalance = queries.queryBalances - .getQueryBech32Address(osmosisAddress) + // If we get here destination address is defined + .getQueryBech32Address(destinationAddress!) .balances.find((bal) => { return ( bal.currency.coinMinimalDenom === - assetToBridge.balance.currency.coinMinimalDenom + availableBalance?.currency.coinMinimalDenom ); }); @@ -548,11 +534,11 @@ export const useBridgeQuote = ({ toChainId: quote.toChain.chainId, }); - if (isWithdraw) { - withdrawAmountConfig.setAmount(""); - } else { - setDepositAmount(""); - } + // if (isWithdraw) { + // withdrawAmountConfig.setAmount(""); + // } else { + // setDepositAmount(""); + // } setTransferInitiated(true); } } @@ -567,16 +553,14 @@ export const useBridgeQuote = ({ if (!transactionRequest || !quote) return; - try { - if (transactionRequest.type === "evm") { - await handleEvmTx({ ...quote, transactionRequest }); - } else if (transactionRequest.type === "cosmos") { - await handleCosmosTx({ - ...quote, - transactionRequest, - }); - } - } catch (e) {} + if (transactionRequest.type === "evm") { + await handleEvmTx({ ...quote, transactionRequest }); + } else if (transactionRequest.type === "cosmos") { + await handleCosmosTx({ + ...quote, + transactionRequest, + }); + } }; const hasNoQuotes = someError?.message.includes( @@ -584,21 +568,23 @@ export const useBridgeQuote = ({ ); const warnUserOfSlippage = selectedQuote?.isSlippageTooHigh; const warnUserOfPriceImpact = selectedQuote?.isPriceImpactTooHigh; + const isCorrectEvmChainSelected = + sourceChain?.chainType === "evm" + ? currentEvmChainId === sourceChain?.chainId + : true; let buttonErrorMessage: string | undefined; - if (!counterpartyAddress) { + if (!sourceAddress) { buttonErrorMessage = t("assets.transfer.errors.missingAddress"); - } else if (!isCounterpartyAddressValid) { - buttonErrorMessage = t("assets.transfer.errors.invalidAddress"); } else if (hasNoQuotes) { buttonErrorMessage = t("assets.transfer.errors.noQuotesAvailable"); - } else if (userDisconnectedEthWallet) { + } else if (!isEvmWalletConnected) { buttonErrorMessage = t("assets.transfer.errors.reconnectWallet", { - walletName: ethWalletClient?.displayInfo.displayName ?? "Unknown", + walletName: evmConnector?.name ?? "Unknown", }); - } else if (isDeposit && !isCorrectChainSelected) { + } else if (isDeposit && !isCorrectEvmChainSelected) { buttonErrorMessage = t("assets.transfer.errors.wrongNetworkInWallet", { - walletName: ethWalletClient?.displayInfo.displayName ?? "Unknown", + walletName: evmConnector?.name ?? "Unknown", }); } else if (Boolean(someError)) { buttonErrorMessage = t("assets.transfer.errors.unexpectedError"); @@ -617,11 +603,11 @@ export const useBridgeQuote = ({ quoteResults.some((quoteResult) => quoteResult.fetchStatus !== "idle"); const isLoadingBridgeTransaction = bridgeTransaction.isLoading && bridgeTransaction.fetchStatus !== "idle"; - const isWithdrawReady = isWithdraw && osmosisAccount?.txTypeInProgress === ""; + const isWithdrawReady = isWithdraw && !isSendTxPending; const isDepositReady = isDeposit && - !userDisconnectedEthWallet && - isCorrectChainSelected && + !isEvmWalletConnected && + isCorrectEvmChainSelected && !isLoadingBridgeQuote && !isEthTxPending; const userCanInteract = isDepositReady || isWithdrawReady; @@ -643,17 +629,34 @@ export const useBridgeQuote = ({ !isEthTxPending ) { buttonText = t("assets.transfer.givePermission"); - } else if (isWithdraw) { - buttonText = t("assets.transfer.titleWithdraw", { - coinDenom: originCurrency.coinDenom, - }); } else { - buttonText = t("assets.transfer.titleDeposit", { - coinDenom: originCurrency.coinDenom, - }); + buttonText = `Review ${direction === "deposit" ? "deposit" : "withdraw"}`; } if (selectedQuote && !selectedQuote.expectedOutput) { throw new Error("Expected output is not defined."); } + + return { + buttonText, + buttonErrorMessage, + + userCanInteract, + onTransfer, + + isApprovingToken, + + isInsufficientFee, + isInsufficientBal, + warnUserOfSlippage, + warnUserOfPriceImpact, + + bridgeProviders, + selectedBridgeProvider, + setSelectedBridgeProvider, + + selectedQuote, + isLoadingBridgeQuote, + isLoadingBridgeTransaction, + }; }; diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts index b2afccd991..1bdbf0ba2f 100644 --- a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -24,7 +24,6 @@ export const useBridgesSupportedAssets = ({ address: asset.coinMinimalDenom, decimals: asset.coinDecimals, denom: asset.coinDenom, - sourceDenom: asset.sourceDenom, }, chain, }, diff --git a/packages/web/hooks/evm-wallet.ts b/packages/web/hooks/evm-wallet.ts index 0f03675202..55625543bf 100644 --- a/packages/web/hooks/evm-wallet.ts +++ b/packages/web/hooks/evm-wallet.ts @@ -1,8 +1,15 @@ -import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi"; +import { + useAccount, + useConnect, + useDisconnect, + useSendTransaction, + useSwitchChain, +} from "wagmi"; export const useConnectEvmWallet = useConnect; export const useEvmWalletAccount = useAccount; export const useSwitchEvmChain = useSwitchChain; export const useDisconnectEvmWallet = useDisconnect; +export const useSendEvmTransaction = useSendTransaction; export type ConnectEvmWalletReturn = ReturnType; diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 5106e68dae..3d1ec9fa91 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -95,7 +95,6 @@ export const useFeatureFlags = () => { !isMobile && launchdarklyFlags.swapToolSimulateFee && // 1-Click trading is dependent on the swap tool simulate fee flag launchdarklyFlags.oneClickTrading, - newDepositWithdrawFlow: false, _isInitialized: isDevModeWithoutClientID ? true : isInitialized, _isClientIDPresent: !!process.env.NEXT_PUBLIC_LAUNCH_DARKLY_CLIENT_SIDE_ID, } as Record; diff --git a/packages/web/package.json b/packages/web/package.json index d72c62d565..b861e431fd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -138,8 +138,8 @@ "sharp": "^0.30.4", "tailwindcss-animate": "^1.0.7", "utility-types": "^3.10.0", - "viem": "2.13.3", - "wagmi": "^2.9.6", + "viem": "2.16.4", + "wagmi": "^2.10.8", "web3-utils": "^1.7.4", "zod": "^3.22.4" }, diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index f3351ed820..3477100605 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -4,9 +4,9 @@ import { BridgeChain, BridgeCoin, BridgeProviders, - bridgeSupportedAssetsSchema, getBridgeExternalUrlSchema, getBridgeQuoteSchema, + getBridgeSupportedAssetsParams, } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY, @@ -202,7 +202,7 @@ export const bridgeTransferRouter = createTRPCRouter({ }), getSupportedAssetsByBridge: publicProcedure - .input(bridgeSupportedAssetsSchema.extend({ bridge: z.string() })) + .input(getBridgeSupportedAssetsParams.extend({ bridge: z.string() })) .query(async ({ input, ctx }) => { const bridgeProviders = new BridgeProviders( process.env.NEXT_PUBLIC_SQUID_INTEGRATOR_ID!, diff --git a/packages/web/server/api/routers/local-bridge-transfer.ts b/packages/web/server/api/routers/local-bridge-transfer.ts index 7e0270c3ac..59902ae370 100644 --- a/packages/web/server/api/routers/local-bridge-transfer.ts +++ b/packages/web/server/api/routers/local-bridge-transfer.ts @@ -9,8 +9,8 @@ import { captureErrorAndReturn, DEFAULT_VS_CURRENCY, getAsset, - getAssetWithUserBalance, getEvmBalance, + queryBalances, } from "@osmosis-labs/server"; import { createTRPCRouter, @@ -147,21 +147,66 @@ export const localBridgeTransferRouter = createTRPCRouter({ asset.chainType !== "evm" ) .map(async (asset) => { - const assetWithBalance = await getAssetWithUserBalance({ + const { balances } = await queryBalances({ ...ctx, - userCosmosAddress: input.userCosmosAddress, chainId: asset.chainId, - asset: getAsset({ ...ctx, anyDenom: asset.denom }), + bech32Address: input.userCosmosAddress!, }); + const balance = balances.find((a) => a.denom === asset.address); + + if (!balance) { + return { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.denom, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + } + + const representativeAssetMinimalDenom = + asset.supportedVariants[0]; + const representativeAsset = getAsset({ + ...ctx, + anyDenom: representativeAssetMinimalDenom, + }); + + // is user asset, include user data + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: representativeAsset.coinMinimalDenom, + amount: balance.amount, + }).catch((e) => captureErrorAndReturn(e, undefined)); + return { ...asset, amount: - assetWithBalance.amount ?? - new CoinPretty(assetWithBalance, new Dec(0)), - usdValue: - assetWithBalance.usdValue ?? - new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(balance.amount) + ) ?? + new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty( + DEFAULT_VS_CURRENCY, + usdValue ?? new Dec(0) + ), }; }) ); diff --git a/packages/web/utils/ethereum.ts b/packages/web/utils/ethereum.ts new file mode 100644 index 0000000000..0c9733ac95 --- /dev/null +++ b/packages/web/utils/ethereum.ts @@ -0,0 +1,40 @@ +import { + ResourceUnavailableRpcError, + UnauthorizedProviderError, + UserRejectedRequestError, +} from "viem"; +import { BaseError } from "wagmi"; + +import { MultiLanguageT } from "~/hooks"; + +export function getWagmiToastErrorMessage({ + error, + t, + walletName, +}: { + error: BaseError; + t: MultiLanguageT; + walletName: string; +}) { + if (error.name === UserRejectedRequestError.name) { + return { + titleTranslationKey: "transactionFailed", + captionTranslationKey: "requestRejected", + }; + } else if (error.name === UnauthorizedProviderError.name) { + return { + titleTranslationKey: "Action Unavailable", + captionTranslationKey: "Please log into MetaMask", + }; + } else if (error.name === ResourceUnavailableRpcError.name) { + return { + titleTranslationKey: t("assets.transfer.errors.seeRequest", { + walletName, + }), + }; + } else { + return { + titleTranslationKey: "transactionFailed", + }; + } +} diff --git a/yarn.lock b/yarn.lock index 03f3c9c9fc..319827e93c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,10 +2429,10 @@ long "^4.0.0" protobufjs "~6.11.2" -"@coinbase/wallet-sdk@4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.0.2.tgz#403b2194ecc9bcf8b8fd217ec5cd6529013b58f4" - integrity sha512-WMUeFbtS0rn8zavjAmNhFWq1r3TV7E5KuSij1Sar0/XuOC+nhj96uqSlIApAHdhuScoKZBq39VYsAQCHzOC6/w== +"@coinbase/wallet-sdk@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.0.4.tgz#634cd89bac93eeaf381a1f026476794e53431ed6" + integrity sha512-74c040CRnGhfRjr3ArnkAgud86erIqdkPHNt5HR1k9u97uTIZCJww9eGYT67Qf7gHPpGS/xW8Be1D4dvRm63FA== dependencies: buffer "^6.0.3" clsx "^1.2.1" @@ -6322,10 +6322,10 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.0.0.tgz#8c2b9073fe0722d48693143b0dc8448840daa3bd" integrity sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ== -"@metamask/sdk-communication-layer@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.20.2.tgz#7f7fd334b2d26abd1a5a1ec1ffadf823a9589344" - integrity sha512-TN+whYbCClFSkx52Ild1RcjoRyz8YZgwNvZeooIcZIvCfBM6U9W5273KGiY7WLc/oO4KKmFk17d7vMO4gNvhhw== +"@metamask/sdk-communication-layer@0.26.2": + version "0.26.2" + resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.26.2.tgz#e36f298e78fa7c276a4db6a5bfa76085af3f75ef" + integrity sha512-YMqwjhCZ4sXYAsEp1LxLrZZycBwpUeEsA4yIx48m1yW9sZ8pv3NGnbjM+F0zf29DLjyqLxJdxHJ7b5YkgtB26g== dependencies: bufferutil "^4.0.8" date-fns "^2.29.3" @@ -6333,22 +6333,22 @@ utf-8-validate "^6.0.3" uuid "^8.3.2" -"@metamask/sdk-install-modal-web@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.20.2.tgz#1cf0eb3c26291de7598190878fa9a893c4eb2d66" - integrity sha512-0QiaZhV15AGdN1zU2jfTI32eC3YkwEpzDfR9+oiZ9bd2G72c6lYBhTsmDGUd01aP6A+bqJR5PjI8Wh2AWtoLeA== +"@metamask/sdk-install-modal-web@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.26.0.tgz#412a89747a96e94233eb59d2779ab26656096688" + integrity sha512-LyDQFIsWWyU0ZgZR3O9LzRqKzXcYUEGJRCNfb26IjFOquvmQosbhQV0jDNlVa8Tk2Fg4ykTPoaauANh6sVJYVQ== dependencies: qr-code-styling "^1.6.0-rc.1" -"@metamask/sdk@0.20.3": - version "0.20.3" - resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.20.3.tgz#73851d68ffe5d45c1872c024182922530b187b7a" - integrity sha512-HZ9NwA+LxiXzuy0YWbWsuD4xejQtp85bhcCAf8UgpA/0dOyF3RS4dKDdBBXSyRgk3RWPjeJgHxioaH4CmBmiRA== +"@metamask/sdk@0.26.3": + version "0.26.3" + resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.26.3.tgz#d7b08955f0b2fcb1248913a292490745d52fd57b" + integrity sha512-DM4BFPr1BDAIhTz7/RWb3oWQRvX79TJVZH8EL/Ljp+CRY7IjCbaVwaLdyQjVd8Doyq1V7AL4N/JjXplpo2YyYg== dependencies: "@metamask/onboarding" "^1.0.1" "@metamask/providers" "^15.0.0" - "@metamask/sdk-communication-layer" "0.20.2" - "@metamask/sdk-install-modal-web" "0.20.2" + "@metamask/sdk-communication-layer" "0.26.2" + "@metamask/sdk-install-modal-web" "0.26.0" "@types/dom-screen-wake-lock" "^1.0.0" bowser "^2.9.0" cross-fetch "^4.0.0" @@ -6806,7 +6806,7 @@ dependencies: "@noble/hashes" "1.3.3" -"@noble/curves@^1.0.0", "@noble/curves@^1.1.0", "@noble/curves@^1.2.0", "@noble/curves@^1.4.0", "@noble/curves@~1.4.0": +"@noble/curves@1.4.0", "@noble/curves@^1.0.0", "@noble/curves@^1.1.0", "@noble/curves@^1.2.0", "@noble/curves@^1.4.0", "@noble/curves@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== @@ -8152,7 +8152,7 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" -"@scure/bip32@^1.3.0", "@scure/bip32@^1.3.1": +"@scure/bip32@1.4.0", "@scure/bip32@^1.3.0", "@scure/bip32@^1.3.1": version "1.4.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== @@ -8185,7 +8185,7 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" -"@scure/bip39@^1.2.0", "@scure/bip39@^1.2.1": +"@scure/bip39@1.3.0", "@scure/bip39@^1.2.0", "@scure/bip39@^1.2.1": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== @@ -10773,23 +10773,23 @@ abitype "0.8.7" eventemitter3 "^4.0.7" -"@wagmi/connectors@5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-5.0.5.tgz#040a4e2e6858d9d6dcf0c0f36d20cc07f8b49da1" - integrity sha512-EjMsmPeu4iYDSSfpvsCbpIwhns+E2FrMqujpcgqTboWkAeSoUEbhoAsSwmivMts+5XojOX8NTs6/KP4zQriolg== +"@wagmi/connectors@5.0.20": + version "5.0.20" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-5.0.20.tgz#01620a10d1d3fb15d7d9e06bb007f11c0dfdc7a5" + integrity sha512-H4jleQIiMFy1mUt+6vTtdL8Fw1yve1+b0c30RsfddgaoXjUaTHAeTUmDsGBPADRJfbg3pixLh6xt82z4Mab9hw== dependencies: - "@coinbase/wallet-sdk" "4.0.2" - "@metamask/sdk" "0.20.3" + "@coinbase/wallet-sdk" "4.0.4" + "@metamask/sdk" "0.26.3" "@safe-global/safe-apps-provider" "0.18.1" "@safe-global/safe-apps-sdk" "8.1.0" "@walletconnect/ethereum-provider" "2.13.0" "@walletconnect/modal" "2.6.2" cbw-sdk "npm:@coinbase/wallet-sdk@3.9.3" -"@wagmi/core@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.10.3.tgz#5184c94e2368b984d9ff8911bb83a3c9a92dc7d5" - integrity sha512-Sx5tWFzbLnwJk/aYPsaG8o4SQ8pVs5ucV5AVyPzA9Ibg3+J1P7qxOcfwPDXSNk67vmCGyZWlmBF/IwQChOJYbQ== +"@wagmi/core@2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.11.5.tgz#9fd47cfaeb26ae1546f705f871fcee44f0bd2b91" + integrity sha512-RmtZQkNf/ozdngyDST33WLTdKQHny9SsiNmxln8G06pbnOuhO4dDhnXnfiJ8Lh9GVIfFsjlmtqzfAIo1/86dqg== dependencies: eventemitter3 "5.0.1" mipd "0.0.5" @@ -11958,10 +11958,10 @@ abitype@0.9.8: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== -abitype@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" - integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== +abitype@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" + integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== abort-controller@^3.0.0: version "3.0.0" @@ -26162,19 +26162,19 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" -viem@2.13.3: - version "2.13.3" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.3.tgz#950426e4cacf5e12fab2c202a339371901712481" - integrity sha512-3tlwDRKHSelupFjbFMdUxF41f79ktyH2F9PAQ9Dltbs1DpdDlR1x+Ksa0th6qkyjjAbpDZP3F5nMTJv/1GVPdQ== +viem@2.16.4: + version "2.16.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.16.4.tgz#5cc7bbed81ee815a131f509066972a8e849da801" + integrity sha512-5Dk8BUCUymVJRETeU4rNHhTIj+VtBEWBKU0veJ7URtLPltO8wY0/OaUQeN77OeMhmy/l1z0Gbrm9CEURgE06Iw== dependencies: "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "1.0.0" + "@noble/curves" "1.4.0" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + abitype "1.0.5" isows "1.0.4" - ws "8.13.0" + ws "8.17.1" viem@^1.0.0, viem@^1.1.4, viem@^1.20.3, viem@^1.6.0: version "1.21.4" @@ -26207,13 +26207,13 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" -wagmi@^2.9.6: - version "2.9.6" - resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.9.6.tgz#301557a7a6546f79fd14b8c40e878fa4242fa679" - integrity sha512-cRZJrI/N8XoPs5DwWP1JPaXPQnUKOr4q3w8xbbKSw2hv++4VXngyUn+clo5vqa/23AZTWwRo4vcJYJoHtHP9Hw== +wagmi@^2.10.8: + version "2.10.8" + resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.10.8.tgz#236e907e784eeae3dde808c400b62cb6b573fc51" + integrity sha512-25xJCTEQ3ug6tl86MnngzhXOJUo4tJufUUxlnb2qRz+aZFAcRGL+hhuBBZOJ552T49UPF0Hs9c6Rd4BKvwHLrg== dependencies: - "@wagmi/connectors" "5.0.5" - "@wagmi/core" "2.10.3" + "@wagmi/connectors" "5.0.20" + "@wagmi/core" "2.11.5" use-sync-external-store "1.2.0" walker@^1.0.8: @@ -26588,6 +26588,11 @@ ws@8.13.0, ws@^8.11.0: resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + ws@8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" From fb38d8ee98affa5b0bbd550da71e6a1405f215af Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 22:34:59 -0400 Subject: [PATCH 24/33] fix: build --- packages/utils/src/ethereum.ts | 2 + .../immersive/supported-assets-list.tsx | 139 ------------------ packages/web/integrations/axelar/transfer.tsx | 5 +- packages/web/integrations/axelar/types.ts | 9 +- packages/web/integrations/axelar/utils.ts | 4 +- packages/web/integrations/nomic/transfer.tsx | 4 +- packages/web/modals/bridge-transfer-v1.tsx | 4 +- packages/web/modals/select-asset-source.tsx | 4 +- packages/web/modals/transfer-asset-select.tsx | 6 +- .../wallet-select/cosmos-wallet-state.tsx | 3 +- .../web/stores/assets/transfer-ui-config.ts | 12 +- 11 files changed, 28 insertions(+), 164 deletions(-) delete mode 100644 packages/web/components/bridge/immersive/supported-assets-list.tsx diff --git a/packages/utils/src/ethereum.ts b/packages/utils/src/ethereum.ts index 7c12a29689..a26d16a124 100644 --- a/packages/utils/src/ethereum.ts +++ b/packages/utils/src/ethereum.ts @@ -26,6 +26,8 @@ export const NativeEVMTokenConstantAddress = /** Human-displayable global source chain identifiers. * TODO: use global chain IDs instead of display names as keys + * + * @deprecated */ export type AxelarSourceChain = | "Bitcoin" diff --git a/packages/web/components/bridge/immersive/supported-assets-list.tsx b/packages/web/components/bridge/immersive/supported-assets-list.tsx deleted file mode 100644 index 3f8f0c001b..0000000000 --- a/packages/web/components/bridge/immersive/supported-assets-list.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { BridgeChain } from "@osmosis-labs/bridge"; -import classNames from "classnames"; -import { observer } from "mobx-react-lite"; -import { FunctionComponent, useEffect } from "react"; - -import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; -import { useWalletSelect } from "~/hooks"; -import { useEvmWalletAccount } from "~/hooks/evm-wallet"; -import { useStore } from "~/stores"; -import { api, RouterOutputs } from "~/utils/trpc"; - -type SupportedAsset = ReturnType< - typeof useBridgesSupportedAssets ->["supportedAssetsByChainId"][string][number]; - -interface BaseSupportedAssetListProps { - supportedAssets: SupportedAsset[]; - selectedAssetAddress: string; - onSelectAsset: (asset: SupportedAsset) => void; -} - -interface SupportedAssetEvmListProps extends BaseSupportedAssetListProps { - chain: Extract; -} - -interface SupportedAssetCosmosListProps extends BaseSupportedAssetListProps { - chain: Extract; -} - -export const SupportedAssetEvmList = (props: SupportedAssetEvmListProps) => { - const { address: evmAddress } = useEvmWalletAccount(); - - return ; -}; - -export const SupportedAssetCosmosList = observer( - (props: SupportedAssetCosmosListProps) => { - const { chain, supportedAssets } = props; - - const { accountStore } = useStore(); - const { onOpenWalletSelect } = useWalletSelect(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); - - const counterpartyAccountRepo = accountStore.getWalletRepo(chain.chainId); - const counterpartyAccount = accountStore.getWallet(chain.chainId); - - // TODO: Move this to root - useEffect(() => { - if (typeof chain.chainId !== "string" || !!counterpartyAccount?.address) - return; - counterpartyAccountRepo?.connect(wallet?.walletName).catch(() => - onOpenWalletSelect({ - walletOptions: [{ walletType: "cosmos", chainId: chain.chainId }], - }) - ); - }, [ - chain.chainId, - counterpartyAccount?.address, - counterpartyAccountRepo, - onOpenWalletSelect, - wallet?.walletName, - ]); - - const { data: assets, isLoading } = - api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( - { - type: "cosmos", - assets: supportedAssets as Extract< - SupportedAsset, - { chainType: "cosmos" } - >[], - userOsmoAddress: counterpartyAccount?.address!, - }, - { - enabled: !!counterpartyAccount?.address, - } - ); - - if (!assets) return null; - - return ( - - ); - } -); - -interface SupportedAssetInnerListProps extends BaseSupportedAssetListProps { - assets: NonNullable< - RouterOutputs["local"]["bridgeTransfer"]["getSupportedAssetsBalances"] - >; - isLoading: boolean; -} - -const SupportedAssetInnerList: FunctionComponent< - SupportedAssetInnerListProps -> = ({ - supportedAssets, - selectedAssetAddress, - onSelectAsset, - assets, - isLoading, -}) => { - return ( -
- {assets.map((asset, index) => { - const isActive = - asset.amount.currency.coinMinimalDenom === selectedAssetAddress; - return ( - - ); - })} -
- ); -}; diff --git a/packages/web/integrations/axelar/transfer.tsx b/packages/web/integrations/axelar/transfer.tsx index 41602380dc..a756d08b07 100644 --- a/packages/web/integrations/axelar/transfer.tsx +++ b/packages/web/integrations/axelar/transfer.tsx @@ -1,9 +1,8 @@ import { Environment } from "@axelar-network/axelarjs-sdk"; import { WalletStatus } from "@cosmos-kit/core"; import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; -import type { SourceChain } from "@osmosis-labs/bridge"; import { basicIbcTransfer } from "@osmosis-labs/stores"; -import { getKeyByValue } from "@osmosis-labs/utils"; +import { AxelarSourceChain, getKeyByValue } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { FunctionComponent, @@ -57,7 +56,7 @@ export const AxelarTransfer: FunctionComponent< isWithdraw: boolean; ethWalletClient: EthWallet; balanceOnOsmosis: IBCBalance; - selectedSourceChainKey: SourceChain; + selectedSourceChainKey: AxelarSourceChain; onRequestClose: () => void; onRequestSwitchWallet: () => void; isTestNet?: boolean; diff --git a/packages/web/integrations/axelar/types.ts b/packages/web/integrations/axelar/types.ts index b854088125..883d219758 100644 --- a/packages/web/integrations/axelar/types.ts +++ b/packages/web/integrations/axelar/types.ts @@ -1,4 +1,5 @@ -import type { SourceChain, SourceChainTokenConfig } from "@osmosis-labs/bridge"; +import type { SourceChainTokenConfig } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { IS_TESTNET } from "~/config"; @@ -12,7 +13,7 @@ export interface AxelarBridgeConfig { */ sourceChainTokens: SourceChainTokenConfig[]; /** Default source chain to be selected. Defaults to first in `sourceChains` if left `undefined`. */ - defaultSourceChainId?: SourceChain; + defaultSourceChainId?: AxelarSourceChain; /** Ex: `uusdc`. NOTE: Will get currency info from `originCurrency` on the IBC balance (from registrar). * See: https://docs.axelar.dev/resources/mainnet#assets */ @@ -34,7 +35,7 @@ export interface AxelarBridgeConfig { * Testnet API: https://axelartest-lcd.quickapi.com/axelar/nexus/v1beta1/chains?status=1 */ export const AxelarChainIds_SourceChainMap: { - [axelarChainIds: string]: SourceChain; + [axelarChainIds: string]: AxelarSourceChain; } = IS_TESTNET ? { aurora: "Aurora Testnet", @@ -64,7 +65,7 @@ export const AxelarChainIds_SourceChainMap: { * EVM-compatible wallets, like Metamask. */ export const EthClientChainIds_SourceChainMap: { - [ethClientChainIds: string]: SourceChain; + [ethClientChainIds: string]: AxelarSourceChain; } = { "Aurora Testnet": "Aurora Testnet", "Avalanche Fuji Testnet": "Avalanche Fuji Testnet", diff --git a/packages/web/integrations/axelar/utils.ts b/packages/web/integrations/axelar/utils.ts index 735f0bd1fb..141ddabf2a 100644 --- a/packages/web/integrations/axelar/utils.ts +++ b/packages/web/integrations/axelar/utils.ts @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { t } from "~/hooks/language/context"; @@ -6,7 +6,7 @@ import { t } from "~/hooks/language/context"; * @deprecated */ export function waitByTransferFromSourceChain( - sourceChain: SourceChain | "Osmosis" + sourceChain: AxelarSourceChain | "Osmosis" ) { switch (sourceChain) { case "Ethereum": diff --git a/packages/web/integrations/nomic/transfer.tsx b/packages/web/integrations/nomic/transfer.tsx index 8d14c291af..e900d6e9ec 100644 --- a/packages/web/integrations/nomic/transfer.tsx +++ b/packages/web/integrations/nomic/transfer.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { Network, validate } from "bitcoin-address-validation"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; @@ -43,7 +43,7 @@ export const NomicTransfer: FunctionComponent< { isWithdraw: boolean; balanceOnOsmosis: IBCBalance; - selectedSourceChainKey: SourceChain; + selectedSourceChainKey: AxelarSourceChain; onRequestClose: () => void; onRequestSwitchWallet: () => void; isTestNet?: boolean; diff --git a/packages/web/modals/bridge-transfer-v1.tsx b/packages/web/modals/bridge-transfer-v1.tsx index 4c116feadb..77d7eb2821 100644 --- a/packages/web/modals/bridge-transfer-v1.tsx +++ b/packages/web/modals/bridge-transfer-v1.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import { FunctionComponent } from "react"; @@ -39,7 +39,7 @@ export const BridgeTransferV1Modal: FunctionComponent< isWithdraw: boolean; balance: IBCBalance; /** Selected network key. */ - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; walletClient: ObservableWallet | undefined; onRequestSwitchWallet: () => void; } diff --git a/packages/web/modals/select-asset-source.tsx b/packages/web/modals/select-asset-source.tsx index 2f4b1e2762..60552f1aad 100644 --- a/packages/web/modals/select-asset-source.tsx +++ b/packages/web/modals/select-asset-source.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { FunctionComponent, useState } from "react"; @@ -16,7 +16,7 @@ import { ModalBase, ModalBaseProps } from "~/modals/base"; export const SelectAssetSourceModal: FunctionComponent< ModalBaseProps & { initiallySelectedWalletId?: string; - desiredSourceKey?: SourceChain; + desiredSourceKey?: AxelarSourceChain; isWithdraw: boolean; wallets: ObservableWallet[]; fiatRamps?: FiatRampKey[]; diff --git a/packages/web/modals/transfer-asset-select.tsx b/packages/web/modals/transfer-asset-select.tsx index e808894320..e82eea8d7b 100644 --- a/packages/web/modals/transfer-asset-select.tsx +++ b/packages/web/modals/transfer-asset-select.tsx @@ -1,5 +1,5 @@ import { CoinPretty } from "@keplr-wallet/unit"; -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; @@ -26,7 +26,7 @@ export const TransferAssetSelectModal: FunctionComponent< onSelectAsset: ( denom: string, /** `undefined` if IBC asset. */ - sourceChainKey?: SourceChain + sourceChainKey?: AxelarSourceChain ) => void; } > = observer((props) => { @@ -52,7 +52,7 @@ export const TransferAssetSelectModal: FunctionComponent< )?.token.denom) || tokens[0].token.denom ); const [selectedSourceChainKey, setSelectedSourceChainKey] = - useState(null); + useState(null); // set network-select to selected token's default useEffect(() => { diff --git a/packages/web/modals/wallet-select/cosmos-wallet-state.tsx b/packages/web/modals/wallet-select/cosmos-wallet-state.tsx index 0831cf3cdc..0b8c1996c3 100644 --- a/packages/web/modals/wallet-select/cosmos-wallet-state.tsx +++ b/packages/web/modals/wallet-select/cosmos-wallet-state.tsx @@ -23,7 +23,8 @@ import { CosmosWalletRegistry } from "~/config"; import { useFeatureFlags, useTranslation, WalletSelectOption } from "~/hooks"; import { useHasInstalledCosmosWallets } from "~/hooks/use-has-installed-wallets"; import { WalletSelectModalProps } from "~/modals/wallet-select"; -import { ModalView, OnConnectWallet } from "~/modals/wallet-select/utils"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { ModalView } from "~/modals/wallet-select/utils"; import { useStore } from "~/stores"; import { QRCodeView } from "./qr-code-view"; diff --git a/packages/web/stores/assets/transfer-ui-config.ts b/packages/web/stores/assets/transfer-ui-config.ts index 1c6c534279..fe2688ff3d 100644 --- a/packages/web/stores/assets/transfer-ui-config.ts +++ b/packages/web/stores/assets/transfer-ui-config.ts @@ -1,7 +1,7 @@ import { KVStore } from "@keplr-wallet/common"; import { IBCCurrency } from "@keplr-wallet/types"; -import type { SourceChain } from "@osmosis-labs/bridge"; import { makeLocalStorageKVStore } from "@osmosis-labs/stores"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { action, computed, @@ -174,7 +174,7 @@ export class ObservableTransferUIConfig { } if (balance.originBridgeInfo) { - const sourceChainKey: SourceChain = + const sourceChainKey: AxelarSourceChain = (await this.kvStore.get(makeAssetSrcNetworkPreferredKey(coinDenom))) || balance.originBridgeInfo?.defaultSourceChainId || balance.originBridgeInfo.sourceChainTokens[0].id; @@ -271,7 +271,7 @@ export class ObservableTransferUIConfig { onSelectAsset: ( denom: string, /** Is ibc transfer if `undefined`. */ - sourceChainKey?: SourceChain + sourceChainKey?: AxelarSourceChain ) => void ) { const availableAssets = this.assetsStore.ibcBalances.filter( @@ -289,7 +289,7 @@ export class ObservableTransferUIConfig { // override default source chain if prev selected by if (originBridgeInfo && defaultSourceChainId) originBridgeInfo.defaultSourceChainId = - (defaultSourceChainId as SourceChain) ?? undefined; + (defaultSourceChainId as AxelarSourceChain) ?? undefined; return { token: balance, @@ -322,7 +322,7 @@ export class ObservableTransferUIConfig { protected launchSelectAssetSourceModal( direction: TransferDir, balanceOnOsmosis: IBCBalance, - sourceChainKey: SourceChain + sourceChainKey: AxelarSourceChain ) { const wallets = this._ethClientWallets as ObservableWallet[]; const applicableWallets = wallets.filter(({ key }) => @@ -402,7 +402,7 @@ export class ObservableTransferUIConfig { direction: TransferDir, balanceOnOsmosis: IBCBalance, connectedWalletClient: ObservableWallet | undefined, - sourceChainKey: SourceChain, + sourceChainKey: AxelarSourceChain, onRequestSwitchWallet: () => void, onRequestBack?: () => void ) { From 3564c4929c5736f5e679cb700b8bcca26fe4c6d3 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Sat, 29 Jun 2024 16:28:35 -0400 Subject: [PATCH 25/33] feat: display quote remaining time --- .../bridge/immersive/amount-screen.tsx | 88 +++++++++++++++---- .../immersive/bridge-quote-remaining-time.tsx | 77 ++++++++++++++++ .../bridge/immersive/use-bridge-quote.ts | 43 +++++---- packages/web/localizations/de.json | 1 + packages/web/localizations/en.json | 1 + packages/web/localizations/es.json | 1 + packages/web/localizations/fa.json | 1 + packages/web/localizations/fr.json | 1 + packages/web/localizations/gu.json | 1 + packages/web/localizations/hi.json | 1 + packages/web/localizations/ja.json | 1 + packages/web/localizations/ko.json | 1 + packages/web/localizations/pl.json | 1 + packages/web/localizations/pt-br.json | 1 + packages/web/localizations/ro.json | 1 + packages/web/localizations/ru.json | 1 + packages/web/localizations/tr.json | 1 + packages/web/localizations/zh-cn.json | 1 + packages/web/localizations/zh-hk.json | 1 + packages/web/localizations/zh-tw.json | 1 + 20 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 packages/web/components/bridge/immersive/bridge-quote-remaining-time.tsx diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 10ab9ec149..d5715fb58f 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -17,6 +17,7 @@ import { import { Icon } from "~/components/assets"; import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; +import { BridgeQuoteRemainingTime } from "~/components/bridge/immersive/bridge-quote-remaining-time"; import { BridgeWalletSelectModal } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; @@ -366,7 +367,15 @@ export const AmountScreen = observer( toChain, ]); - const { bridgeProviders, selectedQuote } = useBridgeQuote({ + const { + selectedQuote, + buttonErrorMessage, + buttonText, + isLoadingBridgeQuote, + isLoadingBridgeTransaction, + selectedQuoteUpdatedAt, + refetchInterval, + } = useBridgeQuote({ destinationAddress: destinationAccount?.address, destinationChain, destinationAsset: destinationAsset @@ -385,8 +394,6 @@ export const AmountScreen = observer( bridges: sourceAsset?.supportedProviders, }); - console.log(bridgeProviders, selectedQuote); - if ( isLoadingCanonicalAssetPrice || isNil(supportedAssets) || @@ -877,28 +884,77 @@ export const AmountScreen = observer( )} - {/*
-
- + {isLoadingBridgeQuote && ( +
+
+ +

+ {t("transfer.estimatingTime")} +

+
- {t("transfer.estimatingTime")} + {t("transfer.calculatingFees")}
- - - {t("transfer.calculatingFees")} - -
*/} + )} + {!isLoadingBridgeQuote && !isNil(selectedQuote) && ( +
+
+

+ {selectedQuote.estimatedTime.humanize()} ETA +

+
+
+ {!isNil(selectedQuoteUpdatedAt) && ( + + } + /> + )} +
+ + ~ + {( + selectedQuote.transferFeeFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + ?.add(selectedQuote.gasCost ?? new Dec(0)) + .toString()}{" "} + {t("transfer.fees")} + + +
+
+
+ )}
{!walletConnected ? ( connectWalletButton ) : ( <> -
)} {!isLoadingBridgeQuote && !isNil(selectedQuote) && ( -
-
-

- {selectedQuote.estimatedTime.humanize()} ETA -

-
-
- {!isNil(selectedQuoteUpdatedAt) && ( - - } - /> - )} -
- - ~ - {( - selectedQuote.transferFeeFiat ?? - new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) - ) - ?.add(selectedQuote.gasCost ?? new Dec(0)) - .toString()}{" "} - {t("transfer.fees")} - - + {({ open }) => ( + <> + {" "} + +
+ {open ? ( +

+ {t("transfer.transferDetails")} +

+ ) : ( +

+ {selectedQuote.estimatedTime.humanize()} ETA +

+ )} +
+ {!isNil(selectedQuoteUpdatedAt) && ( + + } + /> + )} +
+ {!open && ( + + ~ + {( + selectedQuote.transferFeeFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + ?.add(selectedQuote.gasCost ?? new Dec(0)) + .toString()}{" "} + {t("transfer.fees")} + + )} + +
+
+
+
+ + {selectedQuote.provider.id}

} + isLoading={isRefetchingQuote} + /> + {selectedQuote.estimatedTime.humanize()}

} + isLoading={isRefetchingQuote} + /> + + {selectedQuote.transferFeeFiat + ? `${selectedQuote.transferFeeFiat.toString()} (${selectedQuote.transferFee + .maxDecimals(4) + .toString()})` + : selectedQuote.transferFee + .maxDecimals(4) + .toString()} +

} + isLoading={isRefetchingQuote} + /> + {(selectedQuote.gasCostFiat || selectedQuote.gasCost) && ( + + {selectedQuote.gasCostFiat + ? selectedQuote.gasCostFiat.toString() + : selectedQuote.gasCost + ?.maxDecimals(4) + .toString()} + {selectedQuote.gasCostFiat && selectedQuote.gasCost + ? ` (${selectedQuote.gasCost + .maxDecimals(4) + .toString()})` + : ""} +

+ } + isLoading={isRefetchingQuote} + /> )} - /> -
-
-
+ {selectedQuote.gasCostFiat && + selectedQuote.transferFeeFiat && ( + + {selectedQuote.gasCostFiat + .add(selectedQuote.transferFeeFiat) + .toString()} +

+ } + isLoading={isRefetchingQuote} + /> + )} + + {selectedQuote.expectedOutputFiat.toString()} ( + {selectedQuote.expectedOutput + .maxDecimals(4) + .toString()} + ) +

+ } + isLoading={isRefetchingQuote} + /> + + + )} + )}
@@ -1074,3 +1166,23 @@ const WalletDisplay: FunctionComponent<{
); }; + +const TransferDetailRow: FunctionComponent<{ + label: string; + value: ReactNode; + isLoading: boolean; +}> = ({ label, value, isLoading }) => { + return ( +
+

{label}

+ + {value} + +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index 3eeb13e560..b5d49200a1 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -669,5 +669,6 @@ export const useBridgeQuote = ({ selectedQuote, isLoadingBridgeQuote, isLoadingBridgeTransaction, + isRefetchingQuote: selectedQuoteQuery?.isRefetching ?? false, }; }; diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index dbae762f15..a98da59ca3 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Mehr Einzahlungsmöglichkeiten", "moreWithdrawOptions": "Weitere Auszahlungsoptionen", "convertTo": "Konvertieren zu", + "depositAs": "Einzahlung als", + "withdrawAs": "Abheben als", "recommended": "Empfohlen", "fees": "Gebühren", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Einzahlung abgeschlossen", "failedWithdraw": "Auszahlung fehlgeschlagen", "failedDeposit": "Einzahlung fehlgeschlagen", - "connectionError": "Verbindungsfehler" + "connectionError": "Verbindungsfehler", + "transferDetails": "Überweisungsdetails", + "provider": "Anbieter", + "estimatedTime": "Geschätzte Zeit", + "providerFees": "Anbietergebühren", + "networkFee": "{networkName} Netzwerkgebühr", + "totalFees": "Gesamtkosten", + "estimatedAmountReceived": "Geschätzter erhaltener Betrag" }, "unknownError": "Unbekannter Fehler", "viewExplorer": "Explorer anzeigen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index b1b60f1192..31579e6395 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -953,7 +953,14 @@ "completedDeposit": "Deposit completed", "failedWithdraw": "Withdraw failed", "failedDeposit": "Deposit failed", - "connectionError": "Connection error" + "connectionError": "Connection error", + "transferDetails": "Transfer details", + "provider": "Provider", + "estimatedTime": "Estimated time", + "providerFees": "Provider fees", + "networkFee": "{networkName} Network fee", + "totalFees": "Total fees", + "estimatedAmountReceived": "Estimated amount received" }, "unknownError": "Unknown error", "viewExplorer": "View explorer", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 7dee0f591b..6ebb7f8176 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Más opciones de depósito", "moreWithdrawOptions": "Más opciones de retiro", "convertTo": "Convertir a", + "depositAs": "Depositar como", + "withdrawAs": "Retirarse como", "recommended": "Recomendado", "fees": "honorarios", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Depósito completado", "failedWithdraw": "Retiro fallido", "failedDeposit": "El depósito falló", - "connectionError": "Error de conexión" + "connectionError": "Error de conexión", + "transferDetails": "Detalles de la transferencia", + "provider": "Proveedor", + "estimatedTime": "Hora prevista", + "providerFees": "Tarifas del proveedor", + "networkFee": "{networkName} Tarifa de red", + "totalFees": "Tarifas totales", + "estimatedAmountReceived": "Cantidad estimada recibida" }, "unknownError": "Error desconocido", "viewExplorer": "Ver Explorador", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index b36422d4c1..ddd8f0a8bc 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -921,6 +921,8 @@ "moreDepositOptions": "گزینه های سپرده بیشتر", "moreWithdrawOptions": "گزینه های برداشت بیشتر", "convertTo": "تبدیل به", + "depositAs": "سپرده گذاری به عنوان", + "withdrawAs": "برداشت به عنوان", "recommended": "توصیه شده", "fees": "هزینه ها", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "واریز تکمیل شد", "failedWithdraw": "برداشت ناموفق بود", "failedDeposit": "سپرده گذاری انجام نشد", - "connectionError": "خطای اتصال" + "connectionError": "خطای اتصال", + "transferDetails": "جزئیات انتقال", + "provider": "ارائه دهنده", + "estimatedTime": "زمان تخمین زده شده", + "providerFees": "هزینه های ارائه دهنده", + "networkFee": "هزینه شبکه {networkName}", + "totalFees": "مجموع هزینه ها", + "estimatedAmountReceived": "مبلغ تخمینی دریافت شده" }, "unknownError": "خطای نا شناس", "viewExplorer": "مشاهده جزئیات تراکنش", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 2fd2a16cd5..961d0d99b1 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Plus d'options de dépôt", "moreWithdrawOptions": "Plus d'options de retrait", "convertTo": "Convertir en", + "depositAs": "Dépôt comme", + "withdrawAs": "Se retirer comme", "recommended": "Recommandé", "fees": "frais", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Dépôt complété", "failedWithdraw": "Échec du retrait", "failedDeposit": "Échec du dépôt", - "connectionError": "Erreur de connexion" + "connectionError": "Erreur de connexion", + "transferDetails": "Détails du transfert", + "provider": "Fournisseur", + "estimatedTime": "Temps estimé", + "providerFees": "Frais du fournisseur", + "networkFee": "{networkName} Frais de réseau", + "totalFees": "Total des frais", + "estimatedAmountReceived": "Montant estimé reçu" }, "unknownError": "Erreur inconnue", "viewExplorer": "Voir dans l'exploreur", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index abee6df577..273bb78bc6 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -921,6 +921,8 @@ "moreDepositOptions": "વધુ થાપણ વિકલ્પો", "moreWithdrawOptions": "વધુ ઉપાડ વિકલ્પો", "convertTo": "માં કન્વર્ટ કરો", + "depositAs": "તરીકે જમા", + "withdrawAs": "તરીકે પાછી ખેંચો", "recommended": "ભલામણ કરેલ", "fees": "ફી", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "ડિપોઝિટ પૂર્ણ", "failedWithdraw": "ઉપાડ નિષ્ફળ", "failedDeposit": "ડિપોઝિટ નિષ્ફળ", - "connectionError": "કનેક્શન ભૂલ" + "connectionError": "કનેક્શન ભૂલ", + "transferDetails": "ટ્રાન્સફર વિગતો", + "provider": "પ્રદાતા", + "estimatedTime": "અંદાજિત સમય", + "providerFees": "પ્રદાતા ફી", + "networkFee": "{networkName} નેટવર્ક ફી", + "totalFees": "કુલ ફી", + "estimatedAmountReceived": "પ્રાપ્ત થયેલ અંદાજિત રકમ" }, "unknownError": "અજાણી ભૂલ", "viewExplorer": "સંશોધક જુઓ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 52f3871fd6..03d8766092 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -921,6 +921,8 @@ "moreDepositOptions": "अधिक जमा विकल्प", "moreWithdrawOptions": "अधिक निकासी विकल्प", "convertTo": "में बदलो", + "depositAs": "इस प्रकार जमा करें", + "withdrawAs": "इस प्रकार वापस लें", "recommended": "अनुशंसित", "fees": "फीस", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "जमा पूर्ण हुआ", "failedWithdraw": "निकासी विफल", "failedDeposit": "जमा विफल", - "connectionError": "संपर्क त्रुटि" + "connectionError": "संपर्क त्रुटि", + "transferDetails": "स्थानांतरण विवरण", + "provider": "प्रदाता", + "estimatedTime": "अनुमानित समय", + "providerFees": "प्रदाता शुल्क", + "networkFee": "{networkName} नेटवर्क शुल्क", + "totalFees": "कुल शुल्क", + "estimatedAmountReceived": "अनुमानित प्राप्त राशि" }, "unknownError": "अज्ञात त्रुटि", "viewExplorer": "एक्सप्लोरर देखें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index b57e88dec8..d9f4e0665e 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -921,6 +921,8 @@ "moreDepositOptions": "その他の入金オプション", "moreWithdrawOptions": "引き出しオプションの追加", "convertTo": "に変換", + "depositAs": "入金", + "withdrawAs": "撤退する", "recommended": "推奨", "fees": "手数料", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "入金完了", "failedWithdraw": "引き出しに失敗しました", "failedDeposit": "入金に失敗しました", - "connectionError": "接続エラー" + "connectionError": "接続エラー", + "transferDetails": "振込詳細", + "provider": "プロバイダー", + "estimatedTime": "予定時刻", + "providerFees": "プロバイダー料金", + "networkFee": "{networkName}ネットワーク料金", + "totalFees": "合計料金", + "estimatedAmountReceived": "受け取る推定金額" }, "unknownError": "不明なエラー", "viewExplorer": "エクスプローラーを表示する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 69e60688c8..7d8379b720 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -921,6 +921,8 @@ "moreDepositOptions": "더 많은 입금 옵션", "moreWithdrawOptions": "더 많은 인출 옵션", "convertTo": "로 변환하다", + "depositAs": "다음으로 입금", + "withdrawAs": "다음으로 인출", "recommended": "추천", "fees": "수수료", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "입금완료", "failedWithdraw": "출금 실패", "failedDeposit": "입금 실패", - "connectionError": "연결 오류" + "connectionError": "연결 오류", + "transferDetails": "송금 세부정보", + "provider": "공급자", + "estimatedTime": "예상 시간", + "providerFees": "공급자 수수료", + "networkFee": "{networkName} 네트워크 요금", + "totalFees": "총 수수료", + "estimatedAmountReceived": "예상 수령 금액" }, "unknownError": "알 수 없는 에러", "viewExplorer": "블록 익스플로러 보기", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index ba65454c8e..203e127b06 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Więcej opcji depozytu", "moreWithdrawOptions": "Więcej opcji wypłat", "convertTo": "Konwertuj na", + "depositAs": "Wpłać jako", + "withdrawAs": "Wycofaj jako", "recommended": "Zalecana", "fees": "opłaty", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Depozyt zrealizowany", "failedWithdraw": "Wypłata nie powiodła się", "failedDeposit": "Wpłata nie powiodła się", - "connectionError": "Błąd połączenia" + "connectionError": "Błąd połączenia", + "transferDetails": "Szczegóły przelewu", + "provider": "Dostawca", + "estimatedTime": "Szacowany czas", + "providerFees": "Opłaty dostawcy", + "networkFee": "{networkName} Opłata sieciowa", + "totalFees": "Wszystkie koszty", + "estimatedAmountReceived": "Szacunkowa otrzymana kwota" }, "unknownError": "Nieznany błąd", "viewExplorer": "zobacz eksplorer", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 628d04ec45..e475e60854 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Mais opções de depósito", "moreWithdrawOptions": "Mais opções de retirada", "convertTo": "Converter para", + "depositAs": "Depositar como", + "withdrawAs": "Retirar como", "recommended": "Recomendado", "fees": "tarifas", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Depósito concluído", "failedWithdraw": "Falha na retirada", "failedDeposit": "Falha no depósito", - "connectionError": "Erro de conexão" + "connectionError": "Erro de conexão", + "transferDetails": "Detalhes da transferência", + "provider": "Fornecedor", + "estimatedTime": "Tempo estimado", + "providerFees": "Taxas do provedor", + "networkFee": "{networkName} Taxa de rede", + "totalFees": "Taxas totais", + "estimatedAmountReceived": "Valor estimado recebido" }, "unknownError": "Erro desconhecido", "viewExplorer": "Visualizar explorer", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index ce183aecb0..3f469b1d11 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Mai multe opțiuni de depunere", "moreWithdrawOptions": "Mai multe opțiuni de retragere", "convertTo": "Schimba in", + "depositAs": "Depozit ca", + "withdrawAs": "Retrage ca", "recommended": "Recomandat", "fees": "taxe", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Depozit finalizat", "failedWithdraw": "Retragerea a eșuat", "failedDeposit": "Depunerea nu a reușit", - "connectionError": "Eroare de conexiune" + "connectionError": "Eroare de conexiune", + "transferDetails": "Detalii transfer", + "provider": "Furnizor", + "estimatedTime": "Timpul estimat", + "providerFees": "Taxe de furnizor", + "networkFee": "{networkName} Taxă de rețea", + "totalFees": "Taxele totale", + "estimatedAmountReceived": "Suma estimată primită" }, "unknownError": "Eroare necunoscuta", "viewExplorer": "vezi explorer", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index a51cbf0fb6..fc65a33930 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Больше вариантов депозита", "moreWithdrawOptions": "Больше вариантов вывода", "convertTo": "Перевести в", + "depositAs": "Депозит как", + "withdrawAs": "Вывести как", "recommended": "рекомендуемые", "fees": "сборы", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Депозит завершен", "failedWithdraw": "Вывести не удалось", "failedDeposit": "Депозит не выполнен", - "connectionError": "Ошибка подключения" + "connectionError": "Ошибка подключения", + "transferDetails": "Детали трансфера", + "provider": "Поставщик", + "estimatedTime": "Расчетное время", + "providerFees": "Комиссия поставщика", + "networkFee": "{networkName} Сетевая плата", + "totalFees": "Общая сумма сборов", + "estimatedAmountReceived": "Ориентировочная полученная сумма" }, "unknownError": "Неизвестная ошибка", "viewExplorer": "Посмотреть проводник", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index afda898d7a..4524033025 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -921,6 +921,8 @@ "moreDepositOptions": "Daha fazla para yatırma seçeneği", "moreWithdrawOptions": "Daha fazla para çekme seçeneği", "convertTo": "E dönüşmek", + "depositAs": "Şu şekilde yatırın:", + "withdrawAs": "Olarak geri çekil", "recommended": "Tavsiye edilen", "fees": "ücretler", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "Para yatırma tamamlandı", "failedWithdraw": "Para çekme işlemi başarısız oldu", "failedDeposit": "Para yatırma işlemi başarısız oldu", - "connectionError": "Bağlantı hatası" + "connectionError": "Bağlantı hatası", + "transferDetails": "Aktarım ayrıntıları", + "provider": "Sağlayıcı", + "estimatedTime": "Tahmini süresi", + "providerFees": "Sağlayıcı ücretleri", + "networkFee": "{networkName} Ağ ücreti", + "totalFees": "Toplam ücretler", + "estimatedAmountReceived": "Alınan tahmini miktar" }, "unknownError": "Bilinmeyen hata", "viewExplorer": "gezginde görüntüle", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 5f1f2b0a8e..f3f9ab5e3c 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -921,6 +921,8 @@ "moreDepositOptions": "更多存款选择", "moreWithdrawOptions": "更多提款选项", "convertTo": "转换成", + "depositAs": "存款为", + "withdrawAs": "提款方式", "recommended": "受到推崇的", "fees": "费用", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "存款已完成", "failedWithdraw": "提现失败", "failedDeposit": "存款失败", - "connectionError": "连接错误" + "connectionError": "连接错误", + "transferDetails": "转账详情", + "provider": "提供者", + "estimatedTime": "预计时间", + "providerFees": "供应商费用", + "networkFee": "{networkName}网络费", + "totalFees": "总费用", + "estimatedAmountReceived": "预计收到金额" }, "unknownError": "未知错误", "viewExplorer": "浏览器查看", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 5ba1bc41f8..f15aa9c515 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -921,6 +921,8 @@ "moreDepositOptions": "更多存款選擇", "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", + "depositAs": "存款為", + "withdrawAs": "提款為", "recommended": "受到推崇的", "fees": "費用", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "存款完成", "failedWithdraw": "提現失敗", "failedDeposit": "存款失敗", - "connectionError": "連線錯誤" + "connectionError": "連線錯誤", + "transferDetails": "轉帳詳情", + "provider": "提供者", + "estimatedTime": "預計時間", + "providerFees": "提供者費用", + "networkFee": "{networkName}網路費用", + "totalFees": "總費用", + "estimatedAmountReceived": "預計收到金額" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index df802f65a6..40cd80d145 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -921,6 +921,8 @@ "moreDepositOptions": "更多存款選擇", "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", + "depositAs": "存款為", + "withdrawAs": "提款為", "recommended": "受到推崇的", "fees": "費用", "stepLabels": { @@ -951,7 +953,14 @@ "completedDeposit": "存款完成", "failedWithdraw": "提現失敗", "failedDeposit": "存款失敗", - "connectionError": "連線錯誤" + "connectionError": "連線錯誤", + "transferDetails": "轉帳詳情", + "provider": "提供者", + "estimatedTime": "預計時間", + "providerFees": "提供者費用", + "networkFee": "{networkName}網路費用", + "totalFees": "總費用", + "estimatedAmountReceived": "預計收到金額" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", From 9e9541afde7e8db7917d15521a63d3a1efa15c94 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Sun, 30 Jun 2024 02:29:17 -0400 Subject: [PATCH 27/33] test: update snapshot --- .../complex/assets/__tests__/assets.spec.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts index 90ea9deea3..b93f617acb 100644 --- a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts +++ b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts @@ -95,17 +95,6 @@ describe("getAssetWithVariants", () => { expect(result).toMatchInlineSnapshot(` [ - { - "coinDecimals": 6, - "coinDenom": "USDC", - "coinGeckoId": "usd-coin", - "coinImageUrl": "/tokens/generated/usdc.svg", - "coinMinimalDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", - "coinName": "USDC", - "isUnstable": false, - "isVerified": true, - "variantGroupKey": "USDC", - }, { "coinDecimals": 6, "coinDenom": "USDC.axl", @@ -150,6 +139,17 @@ describe("getAssetWithVariants", () => { "isVerified": true, "variantGroupKey": "USDC", }, + { + "coinDecimals": 6, + "coinDenom": "USDC", + "coinGeckoId": "usd-coin", + "coinImageUrl": "/tokens/generated/usdc.svg", + "coinMinimalDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "coinName": "USDC", + "isUnstable": false, + "isVerified": true, + "variantGroupKey": "USDC", + }, { "coinDecimals": 6, "coinDenom": "USDC.wh", From ce88df4a31db0a29b6dd9d80c63c2f8ac968ac3f Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Sun, 30 Jun 2024 02:35:57 -0400 Subject: [PATCH 28/33] fix: translation --- packages/web/components/bridge/immersive/amount-screen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 1dc6a6f078..cd9a706f4d 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -625,7 +625,7 @@ export const AmountScreen = observer( .hideDenom(true) .toString() : sourceAssetsBalances[0].usdValue.toString()}{" "} - available + {t("transfer.available")}

)} From 0b80cedb020c92eba92f77b355be9f30b0308108 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Sun, 30 Jun 2024 20:36:08 -0400 Subject: [PATCH 29/33] feat: add bridge provider dropdown --- .../bridge/immersive/amount-screen.tsx | 125 ++++++++------- .../immersive/bridge-provider-dropdown.tsx | 142 ++++++++++++++++++ .../bridge/immersive/use-bridge-quote.ts | 51 +++---- .../web/components/buttons/icon-button.tsx | 2 + packages/web/localizations/de.json | 1 + packages/web/localizations/en.json | 1 + packages/web/localizations/es.json | 1 + packages/web/localizations/fa.json | 1 + packages/web/localizations/fr.json | 1 + packages/web/localizations/gu.json | 1 + packages/web/localizations/hi.json | 1 + packages/web/localizations/ja.json | 1 + packages/web/localizations/ko.json | 1 + packages/web/localizations/pl.json | 1 + packages/web/localizations/pt-br.json | 1 + packages/web/localizations/ro.json | 1 + packages/web/localizations/ru.json | 1 + packages/web/localizations/tr.json | 1 + packages/web/localizations/zh-cn.json | 1 + packages/web/localizations/zh-hk.json | 1 + packages/web/localizations/zh-tw.json | 1 + 21 files changed, 256 insertions(+), 81 deletions(-) create mode 100644 packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index cd9a706f4d..6a513d339b 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -17,6 +17,7 @@ import { import { Icon } from "~/components/assets"; import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; +import { BridgeProviderDropdown } from "~/components/bridge/immersive/bridge-provider-dropdown"; import { BridgeQuoteRemainingTime } from "~/components/bridge/immersive/bridge-quote-remaining-time"; import { BridgeWalletSelectModal } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; @@ -369,6 +370,8 @@ export const AmountScreen = observer( const { selectedQuote, + successfulQuotes, + setSelectedBridgeProvider, buttonErrorMessage, buttonText, isLoadingBridgeQuote, @@ -393,6 +396,10 @@ export const AmountScreen = observer( onRequestClose: onClose, inputAmount: cryptoAmount, bridges: sourceAsset?.supportedProviders, + onTransfer: () => { + setCryptoAmount("0"); + setFiatAmount("0"); + }, }); if ( @@ -748,7 +755,7 @@ export const AmountScreen = observer( )} - {!isNil(sourceAsset) && sourceAsset.supportedVariants.length > 0 && ( + {!isNil(sourceAsset) && sourceAsset.supportedVariants.length > 1 && ( {({ open }) => (
@@ -816,65 +823,69 @@ export const AmountScreen = observer( if (index === 0) { return ( + {({ active }) => ( + + )} + + ); + } + + return ( + + {({ active }) => ( - - ); - } - - return ( - - + )} ); } @@ -955,7 +966,15 @@ export const AmountScreen = observer( {selectedQuote.provider.id}

} + value={ + + setSelectedBridgeProvider(bridgeId) + } + /> + } isLoading={isRefetchingQuote} /> ["selectedQuote"] + >; + quotes: ReturnType["successfulQuotes"]; + onSelect: (bridge: Bridge) => void; +} + +export const BridgeProviderDropdown = ({ + selectedQuote, + quotes, + onSelect, +}: Props) => { + const { t } = useTranslation(); + const fastestQuote = useMemo( + () => + quotes.reduce((prev, current) => + prev.data.estimatedTime.asMilliseconds() < + current.data.estimatedTime.asMilliseconds() + ? prev + : current + ), + [quotes] + ); + return ( + + {({ open }) => ( +
+ + {selectedQuote.provider.id}{" "} + + + + {quotes.map( + ( + { + data: { + provider, + estimatedTime, + transferFeeFiat, + gasCostFiat, + expectedOutputFiat, + }, + }, + index + ) => { + const totalFee = transferFeeFiat + ?.add( + gasCostFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + .toString(); + return ( + + {({ active }) => { + const isSelected = + selectedQuote.provider.id === provider.id; + return ( + + ); + }} + + ); + } + )} + +
+ )} +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index b5d49200a1..2924e71001 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -42,6 +42,7 @@ export const useBridgeQuote = ({ bridges = ["Axelar", "Skip", "Squid", "IBC"], onRequestClose, + onTransfer: onTransferProp, }: { direction: "deposit" | "withdraw"; @@ -58,6 +59,7 @@ export const useBridgeQuote = ({ bridges?: Bridge[]; onRequestClose: () => void; + onTransfer?: () => void; }) => { const { accountStore, transferHistoryStore, queriesStore } = useStore(); const { @@ -251,32 +253,32 @@ export const useBridgeQuote = ({ ) ); + const successfulQuotes = useMemo(() => { + return quoteResults.filter( + ( + quote + ): quote is typeof quote & { data: NonNullable } => + quote.isSuccess && !isNil(quote.data) + ); + }, [quoteResults]); + + const erroredQuotes = useMemo(() => { + return quoteResults.filter(({ isError }) => isError); + }, [quoteResults]); + const selectedQuoteQuery = useMemo(() => { - return quoteResults?.find( + return successfulQuotes.find( ({ data: quote }) => quote?.provider.id === selectedBridgeProvider ); - }, [quoteResults, selectedBridgeProvider]); + }, [selectedBridgeProvider, successfulQuotes]); const selectedQuote = useMemo(() => { return selectedQuoteQuery?.data; }, [selectedQuoteQuery]); - const bridgeProviders = useMemo( - () => - quoteResults - .map(({ data }) => data?.provider) - ?.filter((r): r is NonNullable => !!r) - .map(({ id, logoUrl }) => ({ - id: id, - logo: logoUrl, - name: id, - })), - [quoteResults] - ); - - const numSucceeded = quoteResults.filter(({ isSuccess }) => isSuccess).length; + const numSucceeded = successfulQuotes.length; const isOneSuccessful = Boolean(numSucceeded); - const amountOfErrors = quoteResults.filter(({ isError }) => isError).length; + const amountOfErrors = erroredQuotes.length; const isOneErrored = Boolean(amountOfErrors); // if none have returned a resulting quote, find some error @@ -477,13 +479,10 @@ export const useBridgeQuote = ({ toChainId: quote.toChain.chainId, }); + // TODO: Investigate if this is still needed // setLastDepositAccountEvmAddress(ethWalletClient.accountAddress!); - // if (isWithdraw) { - // withdrawAmountConfig.setAmount(""); - // } else { - // setDepositAmount(""); - // } + onTransferProp?.(); setTransferInitiated(true); } catch (e) { const error = e as BaseError; @@ -541,11 +540,7 @@ export const useBridgeQuote = ({ toChainId: quote.toChain.chainId, }); - // if (isWithdraw) { - // withdrawAmountConfig.setAmount(""); - // } else { - // setDepositAmount(""); - // } + onTransferProp?.(); setTransferInitiated(true); } } @@ -662,7 +657,7 @@ export const useBridgeQuote = ({ warnUserOfSlippage, warnUserOfPriceImpact, - bridgeProviders, + successfulQuotes, selectedBridgeProvider, setSelectedBridgeProvider, diff --git a/packages/web/components/buttons/icon-button.tsx b/packages/web/components/buttons/icon-button.tsx index 3d2bd68349..086d3a823d 100644 --- a/packages/web/components/buttons/icon-button.tsx +++ b/packages/web/components/buttons/icon-button.tsx @@ -10,6 +10,8 @@ import { Button } from "~/components/buttons/button"; /** * Renders an icon within a button. + * + * @deprecated Use the iconButton within the ui folder */ export const IconButton = forwardRef< HTMLButtonElement, diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index a98da59ca3..d171ce8b1d 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -924,6 +924,7 @@ "depositAs": "Einzahlung als", "withdrawAs": "Abheben als", "recommended": "Empfohlen", + "fee": "Gebühr", "fees": "Gebühren", "stepLabels": { "asset": "Vermögenswert", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 31579e6395..0ecab65063 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -924,6 +924,7 @@ "depositAs": "Deposit as", "withdrawAs": "Withdraw as", "recommended": "Recommended", + "fee": "fee", "fees": "fees", "stepLabels": { "asset": "Asset", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 6ebb7f8176..980a51dc6e 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -924,6 +924,7 @@ "depositAs": "Depositar como", "withdrawAs": "Retirarse como", "recommended": "Recomendado", + "fee": "tarifa", "fees": "honorarios", "stepLabels": { "asset": "Activo", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index ddd8f0a8bc..b382c45c20 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -924,6 +924,7 @@ "depositAs": "سپرده گذاری به عنوان", "withdrawAs": "برداشت به عنوان", "recommended": "توصیه شده", + "fee": "هزینه", "fees": "هزینه ها", "stepLabels": { "asset": "دارایی", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 961d0d99b1..5fb8af5bdc 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -924,6 +924,7 @@ "depositAs": "Dépôt comme", "withdrawAs": "Se retirer comme", "recommended": "Recommandé", + "fee": "frais", "fees": "frais", "stepLabels": { "asset": "Actif", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 273bb78bc6..071a43a837 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -924,6 +924,7 @@ "depositAs": "તરીકે જમા", "withdrawAs": "તરીકે પાછી ખેંચો", "recommended": "ભલામણ કરેલ", + "fee": "ફી", "fees": "ફી", "stepLabels": { "asset": "એસેટ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 03d8766092..fec0272dd3 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -924,6 +924,7 @@ "depositAs": "इस प्रकार जमा करें", "withdrawAs": "इस प्रकार वापस लें", "recommended": "अनुशंसित", + "fee": "शुल्क", "fees": "फीस", "stepLabels": { "asset": "संपत्ति", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index d9f4e0665e..4d68d4a7d2 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -924,6 +924,7 @@ "depositAs": "入金", "withdrawAs": "撤退する", "recommended": "推奨", + "fee": "手数料", "fees": "手数料", "stepLabels": { "asset": "資産", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 7d8379b720..4b52a70d00 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -924,6 +924,7 @@ "depositAs": "다음으로 입금", "withdrawAs": "다음으로 인출", "recommended": "추천", + "fee": "요금", "fees": "수수료", "stepLabels": { "asset": "유산", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 203e127b06..8bcec5a042 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -924,6 +924,7 @@ "depositAs": "Wpłać jako", "withdrawAs": "Wycofaj jako", "recommended": "Zalecana", + "fee": "opłata", "fees": "opłaty", "stepLabels": { "asset": "Zaleta", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index e475e60854..8ca4ee5fb1 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -924,6 +924,7 @@ "depositAs": "Depositar como", "withdrawAs": "Retirar como", "recommended": "Recomendado", + "fee": "taxa", "fees": "tarifas", "stepLabels": { "asset": "Ativo", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 3f469b1d11..f9449ebb69 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -924,6 +924,7 @@ "depositAs": "Depozit ca", "withdrawAs": "Retrage ca", "recommended": "Recomandat", + "fee": "taxa", "fees": "taxe", "stepLabels": { "asset": "Atu", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index fc65a33930..d5c786cb6b 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -924,6 +924,7 @@ "depositAs": "Депозит как", "withdrawAs": "Вывести как", "recommended": "рекомендуемые", + "fee": "платеж", "fees": "сборы", "stepLabels": { "asset": "Объект", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 4524033025..be03a86709 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -924,6 +924,7 @@ "depositAs": "Şu şekilde yatırın:", "withdrawAs": "Olarak geri çekil", "recommended": "Tavsiye edilen", + "fee": "ücret", "fees": "ücretler", "stepLabels": { "asset": "Varlık", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index f3f9ab5e3c..80db9f71ed 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -924,6 +924,7 @@ "depositAs": "存款为", "withdrawAs": "提款方式", "recommended": "受到推崇的", + "fee": "费用", "fees": "费用", "stepLabels": { "asset": "资产", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index f15aa9c515..e512120047 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -924,6 +924,7 @@ "depositAs": "存款為", "withdrawAs": "提款為", "recommended": "受到推崇的", + "fee": "費用", "fees": "費用", "stepLabels": { "asset": "資產", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 40cd80d145..f263bf8696 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -924,6 +924,7 @@ "depositAs": "存款為", "withdrawAs": "提款為", "recommended": "受到推崇的", + "fee": "費用", "fees": "費用", "stepLabels": { "asset": "資產", From ad185a5d62eba3db7644abc8b4f07cb25cc27a9e Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 1 Jul 2024 00:43:47 -0400 Subject: [PATCH 30/33] feat: add details icons and tooltips --- .../bridge/immersive/amount-screen.tsx | 267 +++++++++++------ .../immersive/bridge-provider-dropdown.tsx | 119 ++++---- .../bridge/immersive/use-bridge-quote.ts | 7 +- packages/web/localizations/en.json | 9 +- packages/web/package.json | 4 +- packages/web/public/icons/sprite.svg | 11 + yarn.lock | 273 ++++++++++++++++-- 7 files changed, 518 insertions(+), 172 deletions(-) diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index 6a513d339b..d275404294 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,4 +1,12 @@ -import { Disclosure, Menu } from "@headlessui/react"; +import { + Disclosure, + DisclosureButton, + DisclosurePanel, + Menu, + MenuButton, + MenuItem, + MenuItems, +} from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; @@ -379,6 +387,10 @@ export const AmountScreen = observer( isRefetchingQuote, selectedQuoteUpdatedAt, refetchInterval, + isInsufficientBal, + isInsufficientFee, + warnUserOfPriceImpact, + warnUserOfSlippage, } = useBridgeQuote({ destinationAddress: destinationAccount?.address, destinationChain, @@ -554,7 +566,12 @@ export const AmountScreen = observer( @@ -568,7 +585,10 @@ export const AmountScreen = observer( onInput={onInput("crypto")} className="w-full border-none bg-transparent text-center" classes={{ - input: "px-0", + input: classNames("px-0", { + "text-rust-300": + isInsufficientBal || isInsufficientFee, + }), trailingSymbol: "ml-1 align-middle text-2xl text-osmoverse-500 text-left absolute right-0", }} @@ -759,7 +779,7 @@ export const AmountScreen = observer( {({ open }) => (
- +
@@ -798,9 +818,9 @@ export const AmountScreen = observer( />
-
+ - + {sourceAsset.supportedVariants.map( (variantCoinMinimalDenom, index) => { const asset = assetsInOsmosis.find( @@ -812,23 +832,24 @@ export const AmountScreen = observer( setDestinationAsset(asset); }; - // Show all as deposit as for now + // Show all as 'deposit as' for now const isConvert = false ?? asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = destinationAsset?.coinDenom === asset.coinDenom; - // Is canonical asset - if (index === 0) { - return ( - - {({ active }) => ( + const isCanonicalAsset = index === 0; + + return ( + + <> + {isCanonicalAsset ? (
{isSelected && dropdownActiveItemIcon} + ) : ( + )} - - ); - } - - return ( - - {({ active }) => ( - - )} - + + ); } )} - +
)}
@@ -914,7 +929,7 @@ export const AmountScreen = observer( {({ open }) => ( <> {" "} - +
{open ? (

@@ -948,6 +963,23 @@ export const AmountScreen = observer( {t("transfer.fees")} )} + {(warnUserOfPriceImpact || warnUserOfSlippage) && ( + + + + )}

- - + + {selectedQuote.estimatedTime.humanize()}

} + value={ +
+ {" "} +

+ {selectedQuote.estimatedTime.humanize()} +

+
+ } isLoading={isRefetchingQuote} /> - {selectedQuote.transferFeeFiat - ? `${selectedQuote.transferFeeFiat.toString()} (${selectedQuote.transferFee - .maxDecimals(4) - .toString()})` - : selectedQuote.transferFee - .maxDecimals(4) - .toString()} + <> + {selectedQuote.transferFee + .toDec() + .equals(new Dec(0)) ? ( +

+ {t("transfer.free")} +

+ ) : ( +

+ {selectedQuote.transferFeeFiat + ? `${selectedQuote.transferFeeFiat.toString()} (${selectedQuote.transferFee + .maxDecimals(4) + .toString()})` + : selectedQuote.transferFee + .maxDecimals(4) + .toString()} +

+ )} + + } + isLoading={isRefetchingQuote} + /> + + + {isNil(selectedQuote.gasCostFiat) && + isNil(selectedQuote.gasCost) ? ( + +
+ +

+ {t("transfer.unknown")} +

+
+
+ ) : ( + <> + {selectedQuote.gasCostFiat + ? selectedQuote.gasCostFiat.toString() + : selectedQuote.gasCost + ?.maxDecimals(4) + .toString()} + {selectedQuote.gasCostFiat && + selectedQuote.gasCost + ? ` (${selectedQuote.gasCost + .maxDecimals(4) + .toString()})` + : ""} + + )}

} isLoading={isRefetchingQuote} /> - {(selectedQuote.gasCostFiat || selectedQuote.gasCost) && ( + + {(selectedQuote.gasCostFiat || + selectedQuote.transferFeeFiat) && ( - {selectedQuote.gasCostFiat - ? selectedQuote.gasCostFiat.toString() - : selectedQuote.gasCost - ?.maxDecimals(4) - .toString()} - {selectedQuote.gasCostFiat && selectedQuote.gasCost - ? ` (${selectedQuote.gasCost - .maxDecimals(4) - .toString()})` - : ""} +

+ {( + selectedQuote?.gasCostFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + .add( + selectedQuote.transferFeeFiat ?? + new PricePretty( + DEFAULT_VS_CURRENCY, + new Dec(0) + ) + ) + .toString()}

} isLoading={isRefetchingQuote} /> )} - {selectedQuote.gasCostFiat && - selectedQuote.transferFeeFiat && ( - - {selectedQuote.gasCostFiat - .add(selectedQuote.transferFeeFiat) - .toString()} -

- } - isLoading={isRefetchingQuote} - /> - )} +

{selectedQuote.expectedOutputFiat.toString()} ( {selectedQuote.expectedOutput .maxDecimals(4) @@ -1046,7 +1135,7 @@ export const AmountScreen = observer( } isLoading={isRefetchingQuote} /> - + )} diff --git a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx index 801622e8ad..072cdbc84e 100644 --- a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx +++ b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx @@ -1,4 +1,4 @@ -import { Menu } from "@headlessui/react"; +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Dec, PricePretty } from "@keplr-wallet/unit"; import { Bridge } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; @@ -38,7 +38,13 @@ export const BridgeProviderDropdown = ({

{({ open }) => (
- + + {`${selectedQuote.provider.id} {selectedQuote.provider.id}{" "} - - + + {quotes.map( ( { @@ -72,69 +81,61 @@ export const BridgeProviderDropdown = ({ new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) ) .toString(); + const isSelected = selectedQuote.provider.id === provider.id; return ( - - {({ active }) => { - const isSelected = - selectedQuote.provider.id === provider.id; - return ( - - ); - }} - +
+

{expectedOutputFiat.toString()}

+

+ ~{totalFee} {t("transfer.fee")} +

+
+ + ); } )} -
+
)}
diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index 2924e71001..362530dcc2 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -107,6 +107,11 @@ export const useBridgeQuote = ({ const [isBridgeProviderControlledMode, setBridgeProviderControlledMode] = useState(false); + const onChangeBridgeProvider = useCallback((bridge: Bridge) => { + setSelectedBridgeProvider(bridge); + setBridgeProviderControlledMode(true); + }, []); + // Input const [debouncedInputValue, setDebouncedInputValue] = useState(inputAmountRaw); @@ -659,7 +664,7 @@ export const useBridgeQuote = ({ successfulQuotes, selectedBridgeProvider, - setSelectedBridgeProvider, + setSelectedBridgeProvider: onChangeBridgeProvider, selectedQuote, isLoadingBridgeQuote, diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 0ecab65063..7051643f84 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -926,6 +926,11 @@ "recommended": "Recommended", "fee": "fee", "fees": "fees", + "unknown": "Unknown", + "unknownFeeTooltip": "Currently network fees on {networkName} can’t be calculated. Network fees should be displayed in your wallet upon transaction approval.", + "free": "Free", + "cheapest": "Cheapest", + "fastest": "Fastest", "stepLabels": { "asset": "Asset", "amount": "Amount", @@ -961,7 +966,9 @@ "providerFees": "Provider fees", "networkFee": "{networkName} Network fee", "totalFees": "Total fees", - "estimatedAmountReceived": "Estimated amount received" + "estimatedAmountReceived": "Estimated amount received", + "slippageWarning": "The expected output for this transaction is significantly lower than the input amount, which may result in receiving a different final amount than expected.", + "priceImpactWarning": "The price impact of this transaction is {priceImpact}, which may influence the final amount received." }, "unknownError": "Unknown error", "viewExplorer": "View explorer", diff --git a/packages/web/package.json b/packages/web/package.json index b861e431fd..016a695f8d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -42,7 +42,7 @@ "@cosmos-kit/trust": "2.7.2", "@cosmos-kit/xdefi": "2.4.4", "@ethersproject/abi": "^5.7.0", - "@headlessui/react": "^1.7.8", + "@headlessui/react": "^2.1.1", "@keplr-wallet/common": "0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos": "0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/crypto": "0.12.48", @@ -188,4 +188,4 @@ "typescript": "^5.4.5", "use-debugger-hooks": "^1.3.0" } -} +} \ No newline at end of file diff --git a/packages/web/public/icons/sprite.svg b/packages/web/public/icons/sprite.svg index 352966e858..a865f1f497 100644 --- a/packages/web/public/icons/sprite.svg +++ b/packages/web/public/icons/sprite.svg @@ -1247,5 +1247,16 @@ fill="currentColor" /> + + + diff --git a/yarn.lock b/yarn.lock index 319827e93c..16370c6206 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4689,6 +4689,21 @@ dependencies: "@floating-ui/utils" "^0.2.1" +"@floating-ui/core@^1.6.0": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" + integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== + dependencies: + "@floating-ui/utils" "^0.2.4" + +"@floating-ui/dom@^1.0.0": + version "1.6.7" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015" + integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.4" + "@floating-ui/dom@^1.6.1": version "1.6.3" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" @@ -4704,11 +4719,32 @@ dependencies: "@floating-ui/dom" "^1.6.1" +"@floating-ui/react-dom@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" + integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/react@^0.26.16": + version "0.26.19" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.19.tgz#e3c713bec8a7264caa6f8195e0865f9210f483a1" + integrity sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA== + dependencies: + "@floating-ui/react-dom" "^2.1.1" + "@floating-ui/utils" "^0.2.4" + tabbable "^6.0.0" + "@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== +"@floating-ui/utils@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" + integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== + "@fractalwagmi/popup-connection@^1.0.18": version "1.1.1" resolved "https://registry.yarnpkg.com/@fractalwagmi/popup-connection/-/popup-connection-1.1.1.tgz#2dfff4f3bb89d17947adae597f355faf46c194a9" @@ -4755,13 +4791,15 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@headlessui/react@^1.7.8": - version "1.7.19" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" - integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== +"@headlessui/react@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.1.tgz#5403009bf7cd463a2deeec831c36eb235c32d008" + integrity sha512-808gVNUbRDbDR3GMNPHy+ON0uvR8b9H7IA+Q2UbhOsNCIjgwuwb2Iuv8VPT/1AW0UzLX8g10tN6LhF15GaUJCQ== dependencies: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" + "@floating-ui/react" "^0.26.16" + "@react-aria/focus" "^3.17.1" + "@react-aria/interactions" "^3.21.3" + "@tanstack/react-virtual" "3.5.0" "@humanwhocodes/config-array@^0.11.11": version "0.11.11" @@ -5674,7 +5712,29 @@ buffer "^6.0.3" delay "^4.4.0" -"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos@0.12.12", "@keplr-wallet/cosmos@0.12.28": +"@keplr-wallet/common@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.12.tgz#55030d985b729eac582c0d7203190e25ea2cb3ec" + integrity sha512-AxpwmXdqs083lMvA8j0/V30oTGyobsefNaCou+lP4rCyDdYuXSEux+x2+1AGL9xB3yZfN+4jvEEKJdMwHYEHcQ== + dependencies: + "@keplr-wallet/crypto" "0.12.12" + "@keplr-wallet/types" "0.12.12" + buffer "^6.0.3" + delay "^4.4.0" + mobx "^6.1.7" + +"@keplr-wallet/common@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.28.tgz#1d5d985070aced31a34a6426c9ac4b775081acca" + integrity sha512-ESQorPZw8PRiUXhsrxED+E1FEWkAdc6Kwi3Az7ce204gMBQDI2j0XJtTd4uCUp+C24Em9fk0samdHzdoB4caIg== + dependencies: + "@keplr-wallet/crypto" "0.12.28" + "@keplr-wallet/types" "0.12.28" + buffer "^6.0.3" + delay "^4.4.0" + mobx "^6.1.7" + +"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-/A/wHyYo5gQIW5YkAQYZadEv/12EcAuDclO0KboIb9ti4XFJW6S4VY8LnA16R7DZyBx1cnQknyDm101fUrJfJQ== @@ -5691,6 +5751,40 @@ long "^4.0.0" protobufjs "^6.11.2" +"@keplr-wallet/cosmos@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.12.tgz#72c0505d2327bbf2f5cb51502acaf399b88b4ae3" + integrity sha512-9TLsefUIAuDqqf1WHBt9Bk29rPlkezmLM8P1eEsXGUaHBfuqUrO+RwL3eLA3HGcgNvdy9s8e0p/4CMInH/LLLQ== + dependencies: + "@ethersproject/address" "^5.6.0" + "@keplr-wallet/common" "0.12.12" + "@keplr-wallet/crypto" "0.12.12" + "@keplr-wallet/proto-types" "0.12.12" + "@keplr-wallet/simple-fetch" "0.12.12" + "@keplr-wallet/types" "0.12.12" + "@keplr-wallet/unit" "0.12.12" + bech32 "^1.1.4" + buffer "^6.0.3" + long "^4.0.0" + protobufjs "^6.11.2" + +"@keplr-wallet/cosmos@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.28.tgz#d56e73468256e7276a66bb41f145449dbf11efa1" + integrity sha512-IuqmSBgKgIeWBA0XGQKKs28IXFeFMCrfadCbtiZccNc7qnNr5Y/Cyyk01BPC8Dd1ZyEyAByoICgrxvtGN0GGvA== + dependencies: + "@ethersproject/address" "^5.6.0" + "@keplr-wallet/common" "0.12.28" + "@keplr-wallet/crypto" "0.12.28" + "@keplr-wallet/proto-types" "0.12.28" + "@keplr-wallet/simple-fetch" "0.12.28" + "@keplr-wallet/types" "0.12.28" + "@keplr-wallet/unit" "0.12.28" + bech32 "^1.1.4" + buffer "^6.0.3" + long "^4.0.0" + protobufjs "^6.11.2" + "@keplr-wallet/crypto@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5763,7 +5857,7 @@ resolved "https://registry.npmjs.org/@keplr-wallet/popup/-/popup-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-Q/teyV6vdmpH3SySGd1xrNc/mVGK/tCP5vFEG2I3Y4FDCSV1yD7vcVgUy+tN19Z8EM3goR57V2QlarSOidtdjQ== -"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/proto-types@0.12.12": +"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-fLUJEtDadYJIMBzhMSZpEDTvXqk8wW68TwnUCRAcAooEQEtXPwY5gfo3hcekQEiCYtIu8XqzJ9fg01rp2Z4d3w== @@ -5771,6 +5865,22 @@ long "^4.0.0" protobufjs "^6.11.2" +"@keplr-wallet/proto-types@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.12.tgz#24e0530af7604a90f33a397a82fe500865c76154" + integrity sha512-iAqqNlJpxu/8j+SwOXEH2ymM4W0anfxn+eNeWuqz2c/0JxGTWeLURioxQmCtewtllfHdDHHcoQ7/S+NmXiaEgQ== + dependencies: + long "^4.0.0" + protobufjs "^6.11.2" + +"@keplr-wallet/proto-types@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.28.tgz#2fb2c37749ce7db974f01d07387e966c9b99027d" + integrity sha512-ukti/eCTltPUP64jxtk5TjtwJogyfKPqlBIT3KGUCGzBLIPeYMsffL5w5aoHsMjINzOITjYqzXyEF8LTIK/fmw== + dependencies: + long "^4.0.0" + protobufjs "^6.11.2" + "@keplr-wallet/provider-mock@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-mock/-/provider-mock-0.10.24-ibc.go.v7.hot.fix.tgz#5093451dcf37d0ff0dc195516a72bda700e17176" @@ -5826,12 +5936,27 @@ deepmerge "^4.2.2" long "^4.0.0" -"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/router@0.12.12": +"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/router/-/router-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-bt9weexlbhlh8KsOvbDrvHJ8jtUXrXgB2LX+hEAwjclHQt7PMUhx9a5z0Obd19/ive5G/1M7/ccdPIWxRBpKQw== -"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/types@0.12.12", "@keplr-wallet/types@^0.11.41": +"@keplr-wallet/router@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.12.tgz#92a2c006aec6945ed313575af6b0801f8e84e315" + integrity sha512-Aa1TiVRIEPaqs1t27nCNs5Kz6Ty4CLarVdfqcRWlFQL6zFq33GT46s6K9U4Lz2swVCwdmerSXaq308K/GJHTlw== + +"@keplr-wallet/simple-fetch@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.12.tgz#aacc5c3f22b7ab2804b39e864725294a32f858fd" + integrity sha512-lCOsaI8upMpbusfwJqEK8VIEX77+QE8+8MJVRqoCYwjOTqKGdUH7D1ieZWh+pzvzOnVgedM3lxqdmCvdgU91qw== + +"@keplr-wallet/simple-fetch@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e" + integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA== + +"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-3KUjDMUCscYkvKnC+JsJh9+X0NHlsvBgAghP/uy2p5OGtiULqPBAjWiO+hnBbhis3ZEkzGcCROnnBOoccKd3CQ== @@ -5842,6 +5967,28 @@ long "^4.0.0" secretjs "^0.17.0" +"@keplr-wallet/types@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.12.tgz#f4bd9e710d5e53504f6b53330abb45bedd9c20ae" + integrity sha512-fo6b8j9EXnJukGvZorifJWEm1BPIrvaTLuu5PqaU5k1ANDasm/FL1NaUuaTBVvhRjINtvVXqYpW/rVUinA9MBA== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.28.tgz#eac3c2c9d4560856c5c403a87e67925992a04fbf" + integrity sha512-EcM9d46hYDm3AO4lf4GUbTSLRySONtTmhKb7p88q56OQOgJN3MMjRacEo2p9jX9gpPe7gRIjMUalhAfUiFpZoQ== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@^0.11.41": + version "0.11.64" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.11.64.tgz#5a308c8c019b4e18f894e0f35f0904b60134d605" + integrity sha512-GgzeLDHHfZFyne3O7UIfFHj/uYqVbxAZI31RbBwt460OBbvwQzjrlZwvJW3vieWRAgxKSITjzEDBl2WneFTQdQ== + dependencies: + axios "^0.27.2" + long "^4.0.0" + "@keplr-wallet/unit@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5851,6 +5998,24 @@ big-integer "^1.6.48" utility-types "^3.10.0" +"@keplr-wallet/unit@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.12.tgz#2d7f2e38df4e09c8123dcc0784ffc4b5f4166217" + integrity sha512-fayJcfXWKUnbDZiRJHyuA9GMVS9DymjRlCzlpAJ0+xV0c4Kun/f+9FajL9OQAdPPhnJ7A3KevMI4VHZsd9Yw+A== + dependencies: + "@keplr-wallet/types" "0.12.12" + big-integer "^1.6.48" + utility-types "^3.10.0" + +"@keplr-wallet/unit@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.28.tgz#907c7fa0b49a729cda207fca14fc0a38871cc6c4" + integrity sha512-kpXigHDBJGOmhtPkv9hqsQid9zkFo7OQPeKgO2n8GUlOINIXW6kWG5LXYTi/Yg9Uiw1CQF69gFMuZCJ8IzVHlA== + dependencies: + "@keplr-wallet/types" "0.12.28" + big-integer "^1.6.48" + utility-types "^3.10.0" + "@keplr-wallet/wc-client@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/wc-client/-/wc-client-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -8005,6 +8170,45 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@react-aria/focus@^3.17.1": + version "3.17.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.17.1.tgz#c796a188120421e2fedf438cadacdf463c77ad29" + integrity sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ== + dependencies: + "@react-aria/interactions" "^3.21.3" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + +"@react-aria/interactions@^3.21.3": + version "3.21.3" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.21.3.tgz#a2a3e354a8b894bed7a46e1143453f397f2538d7" + integrity sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + +"@react-aria/ssr@^3.9.4": + version "3.9.4" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.4.tgz#9da8b10342c156e816dbfa4c9e713b21f274d7ab" + integrity sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-aria/utils@^3.24.1": + version "3.24.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.24.1.tgz#9d16023f07c23c41793c9030a9bd203a9c8cf0a7" + integrity sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-stately/utils" "^3.10.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + "@react-native/normalize-color@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.1.0.tgz#939b87a9849e81687d3640c5efa2a486ac266f91" @@ -8056,6 +8260,18 @@ "@react-spring/shared" "~9.7.2" "@react-spring/types" "~9.7.2" +"@react-stately/utils@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.1.tgz#dc8685b4994bef0dc10c37b024074be8afbfba62" + integrity sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-types/shared@^3.23.1": + version "3.23.1" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.23.1.tgz#2f23c81d819d0ef376df3cd4c944be4d6bce84c3" + integrity sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw== + "@reduxjs/toolkit@^1.9.1": version "1.9.7" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.7.tgz#7fc07c0b0ebec52043f8cb43510cf346405f78a6" @@ -9198,6 +9414,13 @@ dependencies: tslib "^2.4.0" +"@swc/helpers@^0.5.0": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7" + integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A== + dependencies: + tslib "^2.4.0" + "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -9253,12 +9476,12 @@ dependencies: "@tanstack/table-core" "8.10.3" -"@tanstack/react-virtual@^3.0.0-beta.60": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz#1ce466f530a10f781871360ed2bf7ff83e664f85" - integrity sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw== +"@tanstack/react-virtual@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.0.tgz#873b5b77cf78af563a4a11e6251ed51ee8868132" + integrity sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw== dependencies: - "@tanstack/virtual-core" "3.5.1" + "@tanstack/virtual-core" "3.5.0" "@tanstack/react-virtual@^3.0.0-beta.63": version "3.0.0-beta.63" @@ -9277,10 +9500,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.63.tgz#fa6ffc8b5b9bfef19006ea330f00456381c30361" integrity sha512-KhhfRYSoQpl0y+2axEw+PJZd/e/9p87PDpPompxcXnweNpt9ZHCT/HuNx7MKM9PVY/xzg9xJSWxwnSCrO+d6PQ== -"@tanstack/virtual-core@3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12" - integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ== +"@tanstack/virtual-core@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.0.tgz#108208d0f1d75271300bc5560cf9a85a1fa01e89" + integrity sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg== "@terra-money/feather.js@^1.0.8": version "1.0.11" @@ -13741,7 +13964,7 @@ cli-width@^3.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -client-only@0.0.1, client-only@^0.0.1: +client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -13827,6 +14050,11 @@ clsx@^1.1.0, clsx@^1.2.1: resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + clsx@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" @@ -24969,6 +25197,11 @@ system-architecture@^0.1.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + tabbable@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.0.1.tgz" From 55007b6880e0a171617d6256490df83ea54103a7 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Mon, 1 Jul 2024 10:40:53 -0400 Subject: [PATCH 31/33] fix build --- packages/web/components/drawers/drawer.tsx | 8 +- yarn.lock | 137 +-------------------- 2 files changed, 8 insertions(+), 137 deletions(-) diff --git a/packages/web/components/drawers/drawer.tsx b/packages/web/components/drawers/drawer.tsx index 6bc4b2344f..7fd852c32d 100644 --- a/packages/web/components/drawers/drawer.tsx +++ b/packages/web/components/drawers/drawer.tsx @@ -1,4 +1,4 @@ -import { Disclosure, Transition } from "@headlessui/react"; +import { Transition } from "@headlessui/react"; import { runIfFn } from "@osmosis-labs/utils"; import classNames from "classnames"; import FocusTrap from "focus-trap-react"; @@ -100,9 +100,7 @@ export const DrawerContent = ( ); }; -export const DrawerOverlay: FunctionComponent< - HTMLProps -> = () => { +export const DrawerOverlay: FunctionComponent = () => { const { isOpen, onClose } = useDrawerProps(); return ( [0]> + props: PropsWithChildren> ) => { const { isOpen, setIsAnimationComplete } = useDrawerProps(); return ( diff --git a/yarn.lock b/yarn.lock index 16370c6206..2f44bb1654 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4791,7 +4791,7 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@headlessui/react@2.1.1": +"@headlessui/react@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.1.tgz#5403009bf7cd463a2deeec831c36eb235c32d008" integrity sha512-808gVNUbRDbDR3GMNPHy+ON0uvR8b9H7IA+Q2UbhOsNCIjgwuwb2Iuv8VPT/1AW0UzLX8g10tN6LhF15GaUJCQ== @@ -5712,29 +5712,7 @@ buffer "^6.0.3" delay "^4.4.0" -"@keplr-wallet/common@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.12.tgz#55030d985b729eac582c0d7203190e25ea2cb3ec" - integrity sha512-AxpwmXdqs083lMvA8j0/V30oTGyobsefNaCou+lP4rCyDdYuXSEux+x2+1AGL9xB3yZfN+4jvEEKJdMwHYEHcQ== - dependencies: - "@keplr-wallet/crypto" "0.12.12" - "@keplr-wallet/types" "0.12.12" - buffer "^6.0.3" - delay "^4.4.0" - mobx "^6.1.7" - -"@keplr-wallet/common@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.28.tgz#1d5d985070aced31a34a6426c9ac4b775081acca" - integrity sha512-ESQorPZw8PRiUXhsrxED+E1FEWkAdc6Kwi3Az7ce204gMBQDI2j0XJtTd4uCUp+C24Em9fk0samdHzdoB4caIg== - dependencies: - "@keplr-wallet/crypto" "0.12.28" - "@keplr-wallet/types" "0.12.28" - buffer "^6.0.3" - delay "^4.4.0" - mobx "^6.1.7" - -"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos@0.12.12", "@keplr-wallet/cosmos@0.12.28": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-/A/wHyYo5gQIW5YkAQYZadEv/12EcAuDclO0KboIb9ti4XFJW6S4VY8LnA16R7DZyBx1cnQknyDm101fUrJfJQ== @@ -5751,40 +5729,6 @@ long "^4.0.0" protobufjs "^6.11.2" -"@keplr-wallet/cosmos@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.12.tgz#72c0505d2327bbf2f5cb51502acaf399b88b4ae3" - integrity sha512-9TLsefUIAuDqqf1WHBt9Bk29rPlkezmLM8P1eEsXGUaHBfuqUrO+RwL3eLA3HGcgNvdy9s8e0p/4CMInH/LLLQ== - dependencies: - "@ethersproject/address" "^5.6.0" - "@keplr-wallet/common" "0.12.12" - "@keplr-wallet/crypto" "0.12.12" - "@keplr-wallet/proto-types" "0.12.12" - "@keplr-wallet/simple-fetch" "0.12.12" - "@keplr-wallet/types" "0.12.12" - "@keplr-wallet/unit" "0.12.12" - bech32 "^1.1.4" - buffer "^6.0.3" - long "^4.0.0" - protobufjs "^6.11.2" - -"@keplr-wallet/cosmos@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.28.tgz#d56e73468256e7276a66bb41f145449dbf11efa1" - integrity sha512-IuqmSBgKgIeWBA0XGQKKs28IXFeFMCrfadCbtiZccNc7qnNr5Y/Cyyk01BPC8Dd1ZyEyAByoICgrxvtGN0GGvA== - dependencies: - "@ethersproject/address" "^5.6.0" - "@keplr-wallet/common" "0.12.28" - "@keplr-wallet/crypto" "0.12.28" - "@keplr-wallet/proto-types" "0.12.28" - "@keplr-wallet/simple-fetch" "0.12.28" - "@keplr-wallet/types" "0.12.28" - "@keplr-wallet/unit" "0.12.28" - bech32 "^1.1.4" - buffer "^6.0.3" - long "^4.0.0" - protobufjs "^6.11.2" - "@keplr-wallet/crypto@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5857,7 +5801,7 @@ resolved "https://registry.npmjs.org/@keplr-wallet/popup/-/popup-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-Q/teyV6vdmpH3SySGd1xrNc/mVGK/tCP5vFEG2I3Y4FDCSV1yD7vcVgUy+tN19Z8EM3goR57V2QlarSOidtdjQ== -"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/proto-types@0.12.12": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-fLUJEtDadYJIMBzhMSZpEDTvXqk8wW68TwnUCRAcAooEQEtXPwY5gfo3hcekQEiCYtIu8XqzJ9fg01rp2Z4d3w== @@ -5865,22 +5809,6 @@ long "^4.0.0" protobufjs "^6.11.2" -"@keplr-wallet/proto-types@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.12.tgz#24e0530af7604a90f33a397a82fe500865c76154" - integrity sha512-iAqqNlJpxu/8j+SwOXEH2ymM4W0anfxn+eNeWuqz2c/0JxGTWeLURioxQmCtewtllfHdDHHcoQ7/S+NmXiaEgQ== - dependencies: - long "^4.0.0" - protobufjs "^6.11.2" - -"@keplr-wallet/proto-types@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.28.tgz#2fb2c37749ce7db974f01d07387e966c9b99027d" - integrity sha512-ukti/eCTltPUP64jxtk5TjtwJogyfKPqlBIT3KGUCGzBLIPeYMsffL5w5aoHsMjINzOITjYqzXyEF8LTIK/fmw== - dependencies: - long "^4.0.0" - protobufjs "^6.11.2" - "@keplr-wallet/provider-mock@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-mock/-/provider-mock-0.10.24-ibc.go.v7.hot.fix.tgz#5093451dcf37d0ff0dc195516a72bda700e17176" @@ -5936,27 +5864,12 @@ deepmerge "^4.2.2" long "^4.0.0" -"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/router@0.12.12": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/router/-/router-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-bt9weexlbhlh8KsOvbDrvHJ8jtUXrXgB2LX+hEAwjclHQt7PMUhx9a5z0Obd19/ive5G/1M7/ccdPIWxRBpKQw== -"@keplr-wallet/router@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.12.tgz#92a2c006aec6945ed313575af6b0801f8e84e315" - integrity sha512-Aa1TiVRIEPaqs1t27nCNs5Kz6Ty4CLarVdfqcRWlFQL6zFq33GT46s6K9U4Lz2swVCwdmerSXaq308K/GJHTlw== - -"@keplr-wallet/simple-fetch@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.12.tgz#aacc5c3f22b7ab2804b39e864725294a32f858fd" - integrity sha512-lCOsaI8upMpbusfwJqEK8VIEX77+QE8+8MJVRqoCYwjOTqKGdUH7D1ieZWh+pzvzOnVgedM3lxqdmCvdgU91qw== - -"@keplr-wallet/simple-fetch@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e" - integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA== - -"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix": +"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/types@0.12.12", "@keplr-wallet/types@^0.11.41": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-3KUjDMUCscYkvKnC+JsJh9+X0NHlsvBgAghP/uy2p5OGtiULqPBAjWiO+hnBbhis3ZEkzGcCROnnBOoccKd3CQ== @@ -5967,28 +5880,6 @@ long "^4.0.0" secretjs "^0.17.0" -"@keplr-wallet/types@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.12.tgz#f4bd9e710d5e53504f6b53330abb45bedd9c20ae" - integrity sha512-fo6b8j9EXnJukGvZorifJWEm1BPIrvaTLuu5PqaU5k1ANDasm/FL1NaUuaTBVvhRjINtvVXqYpW/rVUinA9MBA== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.28.tgz#eac3c2c9d4560856c5c403a87e67925992a04fbf" - integrity sha512-EcM9d46hYDm3AO4lf4GUbTSLRySONtTmhKb7p88q56OQOgJN3MMjRacEo2p9jX9gpPe7gRIjMUalhAfUiFpZoQ== - dependencies: - long "^4.0.0" - -"@keplr-wallet/types@^0.11.41": - version "0.11.64" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.11.64.tgz#5a308c8c019b4e18f894e0f35f0904b60134d605" - integrity sha512-GgzeLDHHfZFyne3O7UIfFHj/uYqVbxAZI31RbBwt460OBbvwQzjrlZwvJW3vieWRAgxKSITjzEDBl2WneFTQdQ== - dependencies: - axios "^0.27.2" - long "^4.0.0" - "@keplr-wallet/unit@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -5998,24 +5889,6 @@ big-integer "^1.6.48" utility-types "^3.10.0" -"@keplr-wallet/unit@0.12.12": - version "0.12.12" - resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.12.tgz#2d7f2e38df4e09c8123dcc0784ffc4b5f4166217" - integrity sha512-fayJcfXWKUnbDZiRJHyuA9GMVS9DymjRlCzlpAJ0+xV0c4Kun/f+9FajL9OQAdPPhnJ7A3KevMI4VHZsd9Yw+A== - dependencies: - "@keplr-wallet/types" "0.12.12" - big-integer "^1.6.48" - utility-types "^3.10.0" - -"@keplr-wallet/unit@0.12.28": - version "0.12.28" - resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.28.tgz#907c7fa0b49a729cda207fca14fc0a38871cc6c4" - integrity sha512-kpXigHDBJGOmhtPkv9hqsQid9zkFo7OQPeKgO2n8GUlOINIXW6kWG5LXYTi/Yg9Uiw1CQF69gFMuZCJ8IzVHlA== - dependencies: - "@keplr-wallet/types" "0.12.28" - big-integer "^1.6.48" - utility-types "^3.10.0" - "@keplr-wallet/wc-client@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/wc-client/-/wc-client-0.10.24-ibc.go.v7.hot.fix.tgz" From 28192b88d2379cef6fe32004cad9a7b159a0dbc0 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Mon, 1 Jul 2024 11:01:04 -0400 Subject: [PATCH 32/33] fix tests & build --- packages/web/__tests__/index-page.spec.tsx | 15 ++++++++++++++- .../swap-tool/__tests__/swap-tool.spec.tsx | 10 ++++++++++ packages/web/localizations/de.json | 9 ++++++++- packages/web/localizations/es.json | 9 ++++++++- packages/web/localizations/fa.json | 9 ++++++++- packages/web/localizations/fr.json | 9 ++++++++- packages/web/localizations/gu.json | 9 ++++++++- packages/web/localizations/hi.json | 9 ++++++++- packages/web/localizations/ja.json | 9 ++++++++- packages/web/localizations/ko.json | 9 ++++++++- packages/web/localizations/pl.json | 9 ++++++++- packages/web/localizations/pt-br.json | 9 ++++++++- packages/web/localizations/ro.json | 9 ++++++++- packages/web/localizations/ru.json | 9 ++++++++- packages/web/localizations/tr.json | 9 ++++++++- packages/web/localizations/zh-cn.json | 9 ++++++++- packages/web/localizations/zh-hk.json | 9 ++++++++- packages/web/localizations/zh-tw.json | 9 ++++++++- packages/web/package.json | 2 +- 19 files changed, 153 insertions(+), 18 deletions(-) diff --git a/packages/web/__tests__/index-page.spec.tsx b/packages/web/__tests__/index-page.spec.tsx index 3ab1202ec7..7d67760b24 100644 --- a/packages/web/__tests__/index-page.spec.tsx +++ b/packages/web/__tests__/index-page.spec.tsx @@ -13,6 +13,16 @@ import HomePage, { PreviousTrade, SwapPreviousTradeKey } from "~/pages"; jest.mock("next/router", () => jest.requireActual("next-router-mock")); +// Mock the ResizeObserver +const ResizeObserverMock = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Stub the global ResizeObserver +global.ResizeObserver = ResizeObserverMock; + const atomAsset = getAssetFromAssetList({ assetLists: AssetLists, sourceDenom: "uatom", @@ -38,7 +48,10 @@ afterEach(() => { beforeEach(() => { server.use( trpcMsw.edge.assets.getUserAssets.query((_req, res, ctx) => { - return res(ctx.status(200), ctx.data({ items: [], nextCursor: null })); + return res( + ctx.status(200), + ctx.data({ items: [], nextCursor: undefined }) + ); }), trpcMsw.edge.assets.getAssetPrice.query((_req, res, ctx) => { return res( diff --git a/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx b/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx index cac3fcbd5e..85e6b877df 100644 --- a/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx +++ b/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx @@ -21,6 +21,16 @@ import { appRouter } from "~/server/api/root-router"; jest.mock("next/router", () => jest.requireActual("next-router-mock")); +// Mock the ResizeObserver +const ResizeObserverMock = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Stub the global ResizeObserver +global.ResizeObserver = ResizeObserverMock; + const createCaller = createCallerFactory(appRouter); const caller = createCaller({ /** diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index d171ce8b1d..274e04b7ed 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -926,6 +926,11 @@ "recommended": "Empfohlen", "fee": "Gebühr", "fees": "Gebühren", + "unknown": "Unbekannt", + "unknownFeeTooltip": "Derzeit können Netzwerkgebühren für {networkName} nicht berechnet werden. Netzwerkgebühren sollten nach der Transaktionsgenehmigung in Ihrem Wallet angezeigt werden.", + "free": "Frei", + "cheapest": "Am billigsten", + "fastest": "Am schnellsten", "stepLabels": { "asset": "Vermögenswert", "amount": "Menge", @@ -961,7 +966,9 @@ "providerFees": "Anbietergebühren", "networkFee": "{networkName} Netzwerkgebühr", "totalFees": "Gesamtkosten", - "estimatedAmountReceived": "Geschätzter erhaltener Betrag" + "estimatedAmountReceived": "Geschätzter erhaltener Betrag", + "slippageWarning": "Der erwartete Ausgabebetrag für diese Transaktion ist erheblich niedriger als der Eingabebetrag. Daher kann es sein, dass Sie einen anderen Endbetrag als erwartet erhalten.", + "priceImpactWarning": "Die Preiseinflüsse dieser Transaktion betragen {priceImpact} und können sich auf den endgültigen Betrag auswirken." }, "unknownError": "Unbekannter Fehler", "viewExplorer": "Explorer anzeigen", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 980a51dc6e..65225fea8a 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -926,6 +926,11 @@ "recommended": "Recomendado", "fee": "tarifa", "fees": "honorarios", + "unknown": "Desconocido", + "unknownFeeTooltip": "Actualmente, las tarifas de red de {networkName} no se pueden calcular. Las tarifas de la red deben aparecer en su billetera al momento de la aprobación de la transacción.", + "free": "Gratis", + "cheapest": "Lo mas barato", + "fastest": "Lo más rápido", "stepLabels": { "asset": "Activo", "amount": "Cantidad", @@ -961,7 +966,9 @@ "providerFees": "Tarifas del proveedor", "networkFee": "{networkName} Tarifa de red", "totalFees": "Tarifas totales", - "estimatedAmountReceived": "Cantidad estimada recibida" + "estimatedAmountReceived": "Cantidad estimada recibida", + "slippageWarning": "El resultado esperado para esta transacción es significativamente menor que el monto de entrada, lo que puede resultar en recibir un monto final diferente al esperado.", + "priceImpactWarning": "El impacto en el precio de esta transacción es {priceImpact} , lo que puede influir en el importe final recibido." }, "unknownError": "Error desconocido", "viewExplorer": "Ver Explorador", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index b382c45c20..b42d816d69 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -926,6 +926,11 @@ "recommended": "توصیه شده", "fee": "هزینه", "fees": "هزینه ها", + "unknown": "ناشناخته", + "unknownFeeTooltip": "در حال حاضر هزینه‌های شبکه برای {networkName} قابل محاسبه نیست. پس از تایید تراکنش، هزینه های شبکه باید در کیف پول شما نمایش داده شود.", + "free": "رایگان", + "cheapest": "ارزان ترین", + "fastest": "سریع ترین", "stepLabels": { "asset": "دارایی", "amount": "میزان", @@ -961,7 +966,9 @@ "providerFees": "هزینه های ارائه دهنده", "networkFee": "هزینه شبکه {networkName}", "totalFees": "مجموع هزینه ها", - "estimatedAmountReceived": "مبلغ تخمینی دریافت شده" + "estimatedAmountReceived": "مبلغ تخمینی دریافت شده", + "slippageWarning": "خروجی مورد انتظار برای این تراکنش به طور قابل توجهی کمتر از مقدار ورودی است که ممکن است منجر به دریافت مبلغ نهایی متفاوتی نسبت به انتظار شود.", + "priceImpactWarning": "تأثیر قیمت این تراکنش {priceImpact} است که ممکن است بر مبلغ نهایی دریافتی تأثیر بگذارد." }, "unknownError": "خطای نا شناس", "viewExplorer": "مشاهده جزئیات تراکنش", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 5fb8af5bdc..fd1762d19b 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -926,6 +926,11 @@ "recommended": "Recommandé", "fee": "frais", "fees": "frais", + "unknown": "Inconnu", + "unknownFeeTooltip": "Actuellement, les frais de réseau sur {networkName} ne peuvent pas être calculés. Les frais de réseau doivent être affichés dans votre portefeuille lors de l'approbation de la transaction.", + "free": "Gratuit", + "cheapest": "Le moins cher", + "fastest": "Le plus rapide", "stepLabels": { "asset": "Actif", "amount": "Montant", @@ -961,7 +966,9 @@ "providerFees": "Frais du fournisseur", "networkFee": "{networkName} Frais de réseau", "totalFees": "Total des frais", - "estimatedAmountReceived": "Montant estimé reçu" + "estimatedAmountReceived": "Montant estimé reçu", + "slippageWarning": "Le résultat attendu pour cette transaction est nettement inférieur au montant d'entrée, ce qui peut entraîner la réception d'un montant final différent de celui attendu.", + "priceImpactWarning": "L'impact sur le prix de cette transaction est {priceImpact} , ce qui peut influencer le montant final reçu." }, "unknownError": "Erreur inconnue", "viewExplorer": "Voir dans l'exploreur", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 071a43a837..3fb49f2b09 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -926,6 +926,11 @@ "recommended": "ભલામણ કરેલ", "fee": "ફી", "fees": "ફી", + "unknown": "અજ્ઞાત", + "unknownFeeTooltip": "હાલમાં {networkName} પરની નેટવર્ક ફીની ગણતરી કરી શકાતી નથી. ટ્રાન્ઝેક્શનની મંજૂરી પર નેટવર્ક ફી તમારા વૉલેટમાં પ્રદર્શિત થવી જોઈએ.", + "free": "મફત", + "cheapest": "સૌથી સસ્તું", + "fastest": "સૌથી ઝડપી", "stepLabels": { "asset": "એસેટ", "amount": "રકમ", @@ -961,7 +966,9 @@ "providerFees": "પ્રદાતા ફી", "networkFee": "{networkName} નેટવર્ક ફી", "totalFees": "કુલ ફી", - "estimatedAmountReceived": "પ્રાપ્ત થયેલ અંદાજિત રકમ" + "estimatedAmountReceived": "પ્રાપ્ત થયેલ અંદાજિત રકમ", + "slippageWarning": "આ ટ્રાન્ઝેક્શન માટે અપેક્ષિત આઉટપુટ ઇનપુટ રકમ કરતાં નોંધપાત્ર રીતે ઓછું છે, જેના પરિણામે અપેક્ષા કરતાં અલગ અંતિમ રકમ પ્રાપ્ત થઈ શકે છે.", + "priceImpactWarning": "આ વ્યવહારની કિંમતની અસર {priceImpact} છે, જે પ્રાપ્ત થયેલી અંતિમ રકમને પ્રભાવિત કરી શકે છે." }, "unknownError": "અજાણી ભૂલ", "viewExplorer": "સંશોધક જુઓ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index fec0272dd3..9a50dbead1 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -926,6 +926,11 @@ "recommended": "अनुशंसित", "fee": "शुल्क", "fees": "फीस", + "unknown": "अज्ञात", + "unknownFeeTooltip": "वर्तमान में {networkName} पर नेटवर्क शुल्क की गणना नहीं की जा सकती। लेन-देन स्वीकृति पर नेटवर्क शुल्क आपके वॉलेट में प्रदर्शित होना चाहिए।", + "free": "मुक्त", + "cheapest": "सबसे सस्ता", + "fastest": "सबसे तेजी से", "stepLabels": { "asset": "संपत्ति", "amount": "मात्रा", @@ -961,7 +966,9 @@ "providerFees": "प्रदाता शुल्क", "networkFee": "{networkName} नेटवर्क शुल्क", "totalFees": "कुल शुल्क", - "estimatedAmountReceived": "अनुमानित प्राप्त राशि" + "estimatedAmountReceived": "अनुमानित प्राप्त राशि", + "slippageWarning": "इस लेनदेन के लिए अपेक्षित आउटपुट इनपुट राशि से काफी कम है, जिसके परिणामस्वरूप अपेक्षित राशि से भिन्न अंतिम राशि प्राप्त हो सकती है।", + "priceImpactWarning": "इस लेनदेन का मूल्य प्रभाव {priceImpact} है, जो प्राप्त अंतिम राशि को प्रभावित कर सकता है।" }, "unknownError": "अज्ञात त्रुटि", "viewExplorer": "एक्सप्लोरर देखें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 4d68d4a7d2..130c525a7c 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -926,6 +926,11 @@ "recommended": "推奨", "fee": "手数料", "fees": "手数料", + "unknown": "未知", + "unknownFeeTooltip": "現在、 {networkName}のネットワーク手数料を計算できません。ネットワーク手数料は、トランザクションが承認されるとウォレットに表示されます。", + "free": "無料", + "cheapest": "最も安い", + "fastest": "最速", "stepLabels": { "asset": "資産", "amount": "額", @@ -961,7 +966,9 @@ "providerFees": "プロバイダー料金", "networkFee": "{networkName}ネットワーク料金", "totalFees": "合計料金", - "estimatedAmountReceived": "受け取る推定金額" + "estimatedAmountReceived": "受け取る推定金額", + "slippageWarning": "このトランザクションの予想出力は入力金額よりも大幅に低いため、予想とは異なる最終金額を受け取る可能性があります。", + "priceImpactWarning": "この取引の価格影響は{priceImpact}であり、最終的に受け取る金額に影響する可能性があります。" }, "unknownError": "不明なエラー", "viewExplorer": "エクスプローラーを表示する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 4b52a70d00..5a7bd3ffae 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -926,6 +926,11 @@ "recommended": "추천", "fee": "요금", "fees": "수수료", + "unknown": "알려지지 않은", + "unknownFeeTooltip": "현재 {networkName} 의 네트워크 요금을 계산할 수 없습니다. 네트워크 수수료는 거래 승인 시 지갑에 표시되어야 합니다.", + "free": "무료", + "cheapest": "가장 저렴", + "fastest": "가장 빠른", "stepLabels": { "asset": "유산", "amount": "양", @@ -961,7 +966,9 @@ "providerFees": "공급자 수수료", "networkFee": "{networkName} 네트워크 요금", "totalFees": "총 수수료", - "estimatedAmountReceived": "예상 수령 금액" + "estimatedAmountReceived": "예상 수령 금액", + "slippageWarning": "이 거래에 대한 예상 출력은 입력 금액보다 상당히 낮으므로 예상과 다른 최종 금액을 받게 될 수 있습니다.", + "priceImpactWarning": "이 거래의 가격 영향은 {priceImpact} 이며, 최종 수령 금액에 영향을 미칠 수 있습니다." }, "unknownError": "알 수 없는 에러", "viewExplorer": "블록 익스플로러 보기", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 8bcec5a042..79d29e2c07 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -926,6 +926,11 @@ "recommended": "Zalecana", "fee": "opłata", "fees": "opłaty", + "unknown": "Nieznany", + "unknownFeeTooltip": "Obecnie nie można obliczyć opłat sieciowych dla {networkName} . Opłaty sieciowe powinny zostać wyświetlone w Twoim portfelu po zatwierdzeniu transakcji.", + "free": "Bezpłatny", + "cheapest": "Najtańszy", + "fastest": "Najszybszy", "stepLabels": { "asset": "Zaleta", "amount": "Kwota", @@ -961,7 +966,9 @@ "providerFees": "Opłaty dostawcy", "networkFee": "{networkName} Opłata sieciowa", "totalFees": "Wszystkie koszty", - "estimatedAmountReceived": "Szacunkowa otrzymana kwota" + "estimatedAmountReceived": "Szacunkowa otrzymana kwota", + "slippageWarning": "Oczekiwany wynik dla tej transakcji jest znacznie niższy niż kwota wejściowa, co może skutkować otrzymaniem kwoty końcowej innej niż oczekiwano.", + "priceImpactWarning": "Wpływ cenowy tej transakcji to {priceImpact} , co może mieć wpływ na ostateczną otrzymaną kwotę." }, "unknownError": "Nieznany błąd", "viewExplorer": "zobacz eksplorer", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 8ca4ee5fb1..d4a0f78309 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -926,6 +926,11 @@ "recommended": "Recomendado", "fee": "taxa", "fees": "tarifas", + "unknown": "Desconhecido", + "unknownFeeTooltip": "Atualmente, as taxas de rede em {networkName} não podem ser calculadas. As taxas de rede devem ser exibidas em sua carteira após a aprovação da transação.", + "free": "Livre", + "cheapest": "Mais barato", + "fastest": "O mais rápido", "stepLabels": { "asset": "Ativo", "amount": "Quantia", @@ -961,7 +966,9 @@ "providerFees": "Taxas do provedor", "networkFee": "{networkName} Taxa de rede", "totalFees": "Taxas totais", - "estimatedAmountReceived": "Valor estimado recebido" + "estimatedAmountReceived": "Valor estimado recebido", + "slippageWarning": "O resultado esperado para esta transação é significativamente inferior ao valor do insumo, o que pode resultar no recebimento de um valor final diferente do esperado.", + "priceImpactWarning": "O impacto no preço desta transação é {priceImpact} , o que pode influenciar o valor final recebido." }, "unknownError": "Erro desconhecido", "viewExplorer": "Visualizar explorer", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index f9449ebb69..0628a369b7 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -926,6 +926,11 @@ "recommended": "Recomandat", "fee": "taxa", "fees": "taxe", + "unknown": "Necunoscut", + "unknownFeeTooltip": "În prezent, taxele de rețea pentru {networkName} nu pot fi calculate. Taxele de rețea ar trebui să fie afișate în portofel după aprobarea tranzacției.", + "free": "Gratuit", + "cheapest": "Cele mai ieftine", + "fastest": "Cel mai rapid", "stepLabels": { "asset": "Atu", "amount": "Cantitate", @@ -961,7 +966,9 @@ "providerFees": "Taxe de furnizor", "networkFee": "{networkName} Taxă de rețea", "totalFees": "Taxele totale", - "estimatedAmountReceived": "Suma estimată primită" + "estimatedAmountReceived": "Suma estimată primită", + "slippageWarning": "Ieșirea așteptată pentru această tranzacție este semnificativ mai mică decât suma de intrare, ceea ce poate duce la primirea unei alte sume finale decât cele așteptate.", + "priceImpactWarning": "Impactul asupra prețului acestei tranzacții este {priceImpact} , care poate influența suma finală primită." }, "unknownError": "Eroare necunoscuta", "viewExplorer": "vezi explorer", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index d5c786cb6b..76a2729f38 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -926,6 +926,11 @@ "recommended": "рекомендуемые", "fee": "платеж", "fees": "сборы", + "unknown": "Неизвестный", + "unknownFeeTooltip": "В настоящее время сетевые сборы для {networkName} не могут быть рассчитаны. Сетевые комиссии должны отображаться в вашем кошельке после одобрения транзакции.", + "free": "Бесплатно", + "cheapest": "Самый дешевый", + "fastest": "Самый быстрый", "stepLabels": { "asset": "Объект", "amount": "Количество", @@ -961,7 +966,9 @@ "providerFees": "Комиссия поставщика", "networkFee": "{networkName} Сетевая плата", "totalFees": "Общая сумма сборов", - "estimatedAmountReceived": "Ориентировочная полученная сумма" + "estimatedAmountReceived": "Ориентировочная полученная сумма", + "slippageWarning": "Ожидаемый результат этой транзакции значительно ниже входной суммы, что может привести к получению другой конечной суммы, чем ожидалось.", + "priceImpactWarning": "Влияние этой транзакции на цену составляет {priceImpact} , что может повлиять на конечную полученную сумму." }, "unknownError": "Неизвестная ошибка", "viewExplorer": "Посмотреть проводник", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index be03a86709..36d18c4961 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -926,6 +926,11 @@ "recommended": "Tavsiye edilen", "fee": "ücret", "fees": "ücretler", + "unknown": "Bilinmeyen", + "unknownFeeTooltip": "Şu anda {networkName} ağ ücretleri hesaplanamıyor. İşlem onaylandıktan sonra ağ ücretleri cüzdanınızda görüntülenmelidir.", + "free": "Özgür", + "cheapest": "En ucuz", + "fastest": "En hızlı", "stepLabels": { "asset": "Varlık", "amount": "Miktar", @@ -961,7 +966,9 @@ "providerFees": "Sağlayıcı ücretleri", "networkFee": "{networkName} Ağ ücreti", "totalFees": "Toplam ücretler", - "estimatedAmountReceived": "Alınan tahmini miktar" + "estimatedAmountReceived": "Alınan tahmini miktar", + "slippageWarning": "Bu işlem için beklenen çıktı, girdi tutarından önemli ölçüde düşüktür ve bu, beklenenden farklı bir nihai tutarın alınmasıyla sonuçlanabilir.", + "priceImpactWarning": "Bu işlemin fiyat etkisi {priceImpact} olup, alınan son tutarı etkileyebilir." }, "unknownError": "Bilinmeyen hata", "viewExplorer": "gezginde görüntüle", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 80db9f71ed..dd366acfb6 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -926,6 +926,11 @@ "recommended": "受到推崇的", "fee": "费用", "fees": "费用", + "unknown": "未知", + "unknownFeeTooltip": "目前无法计算{networkName}上的网络费用。交易批准后,网络费用应显示在您的钱包中。", + "free": "自由的", + "cheapest": "最便宜的", + "fastest": "最快的", "stepLabels": { "asset": "资产", "amount": "数量", @@ -961,7 +966,9 @@ "providerFees": "供应商费用", "networkFee": "{networkName}网络费", "totalFees": "总费用", - "estimatedAmountReceived": "预计收到金额" + "estimatedAmountReceived": "预计收到金额", + "slippageWarning": "该交易的预期输出明显低于输入金额,这可能导致收到的最终金额与预期不同。", + "priceImpactWarning": "此交易的价格影响为{priceImpact} ,可能会影响最终收到的金额。" }, "unknownError": "未知错误", "viewExplorer": "浏览器查看", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index e512120047..9f3181cd71 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -926,6 +926,11 @@ "recommended": "受到推崇的", "fee": "費用", "fees": "費用", + "unknown": "未知", + "unknownFeeTooltip": "目前無法計算{networkName}上的網路費用。交易批准後,網路費用應顯示在您的錢包中。", + "free": "自由的", + "cheapest": "最便宜", + "fastest": "最快的", "stepLabels": { "asset": "資產", "amount": "數量", @@ -961,7 +966,9 @@ "providerFees": "提供者費用", "networkFee": "{networkName}網路費用", "totalFees": "總費用", - "estimatedAmountReceived": "預計收到金額" + "estimatedAmountReceived": "預計收到金額", + "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index f263bf8696..c9b03c3b24 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -926,6 +926,11 @@ "recommended": "受到推崇的", "fee": "費用", "fees": "費用", + "unknown": "未知", + "unknownFeeTooltip": "目前無法計算{networkName}上的網路費用。交易批准後,網路費用應顯示在您的錢包中。", + "free": "自由的", + "cheapest": "最便宜", + "fastest": "最快的", "stepLabels": { "asset": "資產", "amount": "數量", @@ -961,7 +966,9 @@ "providerFees": "提供者費用", "networkFee": "{networkName}網路費用", "totalFees": "總費用", - "estimatedAmountReceived": "預計收到金額" + "estimatedAmountReceived": "預計收到金額", + "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/package.json b/packages/web/package.json index 016a695f8d..4bd704e444 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -188,4 +188,4 @@ "typescript": "^5.4.5", "use-debugger-hooks": "^1.3.0" } -} \ No newline at end of file +} From 1f01db5d635f6e59e0aee2f9770a023053881879 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 1 Jul 2024 13:25:34 -0400 Subject: [PATCH 33/33] feat: add amount and confirmation screen --- .../amount-and-confirmation-screen.tsx | 126 +++++++++++++++ .../bridge/immersive/amount-screen.tsx | 143 ++++++++++-------- .../bridge/immersive/immersive-bridge.tsx | 43 +----- 3 files changed, 213 insertions(+), 99 deletions(-) create mode 100644 packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx diff --git a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx new file mode 100644 index 0000000000..49e1ede849 --- /dev/null +++ b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx @@ -0,0 +1,126 @@ +import { CoinPretty } from "@keplr-wallet/unit"; +import { BridgeChain } from "@osmosis-labs/bridge"; +import { MinimalAsset } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; + +import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { ImmersiveBridgeScreens } from "~/components/bridge/immersive/immersive-bridge"; +import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; +import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; +import { Screen } from "~/components/screen-manager"; +import { Button } from "~/components/ui/button"; +import { useEvmWalletAccount } from "~/hooks/evm-wallet"; +import { useStore } from "~/stores"; + +export type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; + +export type SupportedAssetWithAmount = SupportedAsset & { amount: CoinPretty }; + +interface AmountAndConfirmationScreenProps { + direction: "deposit" | "withdraw"; + selectedAssetDenom: string | undefined; + onClose: () => void; +} + +export const AmountAndConfirmationScreen = observer( + ({ + direction, + selectedAssetDenom, + onClose, + }: AmountAndConfirmationScreenProps) => { + const { accountStore } = useStore(); + + const [sourceAsset, setSourceAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); + const [fromChain, setFromChain] = useState(); + const [toChain, setToChain] = useState(); + + const [cryptoAmount, setCryptoAmount] = useState("0"); + const [fiatAmount, setFiatAmount] = useState("0"); + + // Wallets + const destinationAccount = accountStore.getWallet( + accountStore.osmosisChainId + ); + const { address: evmAddress } = useEvmWalletAccount(); + + const sourceChain = direction === "deposit" ? fromChain : toChain; + const destinationChain = direction === "deposit" ? toChain : fromChain; + + const cosmosCounterpartyAccount = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWallet(sourceChain.chainId); + + const sourceAddress = + sourceChain?.chainType === "evm" + ? evmAddress + : cosmosCounterpartyAccount?.address; + + const quote = useBridgeQuote({ + destinationAddress: destinationAccount?.address, + destinationChain, + destinationAsset: destinationAsset + ? { + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + denom: destinationAsset.coinDenom, + } + : undefined, + sourceAddress, + sourceChain, + sourceAsset, + direction, + onRequestClose: onClose, + inputAmount: cryptoAmount, + bridges: sourceAsset?.supportedProviders, + onTransfer: () => { + setCryptoAmount("0"); + setFiatAmount("0"); + }, + }); + + if (!selectedAssetDenom) return; + + return ( + <> + + {() => ( + + )} + + + {({ goBack }) => ( +
+
Step 3: Review
+ + +
+ )} +
+ + ); + } +); diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index d275404294..a26163fa9d 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -24,6 +24,7 @@ import { } from "react"; import { Icon } from "~/components/assets"; +import { SupportedAssetWithAmount } from "~/components/bridge/immersive/amount-and-confirmation-screen"; import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; import { BridgeProviderDropdown } from "~/components/bridge/immersive/bridge-provider-dropdown"; import { BridgeQuoteRemainingTime } from "~/components/bridge/immersive/bridge-quote-remaining-time"; @@ -56,24 +57,75 @@ interface AmountScreenProps { selectedDenom: string; /** - * Includes both the canonical asset and its variants. + * Chain taking into account the direction. */ - assetsInOsmosis: MinimalAsset[] | undefined; + sourceChain: BridgeChain | undefined; + destinationChain: BridgeChain | undefined; - onClose: () => void; + fromChain: BridgeChain | undefined; + setFromChain: (chain: BridgeChain) => void; + toChain: BridgeChain | undefined; + setToChain: (chain: BridgeChain) => void; + + sourceAsset: SupportedAssetWithAmount | undefined; + setSourceAsset: (asset: SupportedAssetWithAmount | undefined) => void; + destinationAsset: MinimalAsset | undefined; + setDestinationAsset: (asset: MinimalAsset | undefined) => void; + + cryptoAmount: string; + fiatAmount: string; + setCryptoAmount: (amount: string) => void; + setFiatAmount: (amount: string) => void; + + quote: ReturnType; } export const AmountScreen = observer( ({ direction, - assetsInOsmosis, selectedDenom, - onClose, + + sourceChain, + + fromChain, + setFromChain, + toChain, + setToChain, + + sourceAsset, + setSourceAsset, + + destinationAsset, + setDestinationAsset, + + cryptoAmount, + setCryptoAmount, + fiatAmount, + setFiatAmount, + + quote, }: AmountScreenProps) => { const { accountStore } = useStore(); const { onOpenWalletSelect } = useWalletSelect(); const { t } = useTranslation(); + const { + selectedQuote, + successfulQuotes, + setSelectedBridgeProvider, + buttonErrorMessage, + buttonText, + isLoadingBridgeQuote, + isLoadingBridgeTransaction, + isRefetchingQuote, + selectedQuoteUpdatedAt, + refetchInterval, + isInsufficientBal, + isInsufficientFee, + warnUserOfPriceImpact, + warnUserOfSlippage, + } = quote; + const { accountActionButton: connectWalletButton, walletConnected } = useConnectWalletModalRedirect( { @@ -82,18 +134,9 @@ export const AmountScreen = observer( noop ); - const [sourceAsset, setSourceAsset] = useState< - SupportedAsset & { amount: CoinPretty } - >(); - const [destinationAsset, setDestinationAsset] = useState(); - const [fromChain, setFromChain] = useState(); - const [toChain, setToChain] = useState(); - const [areMoreOptionsVisible, setAreMoreOptionsVisible] = useState(false); const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); - const [cryptoAmount, setCryptoAmount] = useState("0"); - const [fiatAmount, setFiatAmount] = useState("0"); const { isOpen: isBridgeWalletSelectOpen, onClose: onCloseBridgeWalletSelect, @@ -110,9 +153,6 @@ export const AmountScreen = observer( isConnected: isEvmWalletConnected, } = useEvmWalletAccount(); - const sourceChain = direction === "deposit" ? fromChain : toChain; - const destinationChain = direction === "deposit" ? toChain : fromChain; - const cosmosCounterpartyAccountRepo = sourceChain?.chainType === "evm" || isNil(sourceChain) ? undefined @@ -127,6 +167,18 @@ export const AmountScreen = observer( ? evmAddress : cosmosCounterpartyAccount?.address; + const { data: assetsInOsmosis } = + api.edge.assets.getCanonicalAssetWithVariants.useQuery( + { + findMinDenomOrSymbol: selectedDenom!, + }, + { + enabled: !isNil(selectedDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ findChainNameOrId: accountStore.osmosisChainId, }); @@ -259,7 +311,7 @@ export const AmountScreen = observer( setDestinationAsset(destinationAsset); } - }, [assetsInOsmosis, selectedDenom, sourceAsset]); + }, [assetsInOsmosis, setDestinationAsset, sourceAsset]); /** * Set the osmosis chain based on the direction @@ -274,13 +326,7 @@ export const AmountScreen = observer( chainType: "cosmos", }); } - }, [ - accountStore.osmosisChainId, - direction, - fromChain, - osmosisChain, - toChain, - ]); + }, [direction, fromChain, osmosisChain, setFromChain, setToChain, toChain]); /** * Set the initial chain based on the direction. @@ -302,7 +348,14 @@ export const AmountScreen = observer( chainType: firstChain.chainType, } as BridgeChain); } - }, [direction, fromChain, supportedChains, toChain]); + }, [ + direction, + fromChain, + setFromChain, + setToChain, + supportedChains, + toChain, + ]); /** * Connect cosmos wallet to the counterparty chain @@ -376,44 +429,6 @@ export const AmountScreen = observer( toChain, ]); - const { - selectedQuote, - successfulQuotes, - setSelectedBridgeProvider, - buttonErrorMessage, - buttonText, - isLoadingBridgeQuote, - isLoadingBridgeTransaction, - isRefetchingQuote, - selectedQuoteUpdatedAt, - refetchInterval, - isInsufficientBal, - isInsufficientFee, - warnUserOfPriceImpact, - warnUserOfSlippage, - } = useBridgeQuote({ - destinationAddress: destinationAccount?.address, - destinationChain, - destinationAsset: destinationAsset - ? { - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, - } - : undefined, - sourceAddress, - sourceChain, - sourceAsset, - direction, - onRequestClose: onClose, - inputAmount: cryptoAmount, - bridges: sourceAsset?.supportedProviders, - onTransfer: () => { - setCryptoAmount("0"); - setFiatAmount("0"); - }, - }); - if ( isLoadingCanonicalAssetPrice || isNil(supportedAssets) || diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index b2e594d113..4b352da241 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -5,11 +5,11 @@ import { memo, PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; import { Icon } from "~/components/assets"; -import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { AmountAndConfirmationScreen } from "~/components/bridge/immersive/amount-and-confirmation-screen"; import { AssetSelectScreen } from "~/components/bridge/immersive/asset-select-screen"; import { Screen, ScreenManager } from "~/components/screen-manager"; import { StepProgress } from "~/components/stepper/progress-bar"; -import { Button, IconButton } from "~/components/ui/button"; +import { IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; import { useTranslation } from "~/hooks"; import { BridgeFlowProvider } from "~/hooks/bridge"; @@ -19,9 +19,8 @@ import { FiatRampKey } from "~/integrations"; import { ModalCloseButton } from "~/modals"; import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; import { FiatRampsModal } from "~/modals/fiat-ramps"; -import { api } from "~/utils/trpc"; -const enum ImmersiveBridgeScreens { +export const enum ImmersiveBridgeScreens { Asset = "0", Amount = "1", Review = "2", @@ -46,18 +45,6 @@ export const ImmersiveBridgeFlow = ({ const [selectedAssetDenom, setSelectedAssetDenom] = useState(); - const { data: canonicalAssetsWithVariants } = - api.edge.assets.getCanonicalAssetWithVariants.useQuery( - { - findMinDenomOrSymbol: selectedAssetDenom!, - }, - { - enabled: !isNil(selectedAssetDenom), - cacheTime: 10 * 60 * 1000, // 10 minutes - staleTime: 10 * 60 * 1000, // 10 minutes - } - ); - const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; assetKey: string; @@ -189,25 +176,11 @@ export const ImmersiveBridgeFlow = ({ /> )} - - {() => ( - setIsVisible(false)} - /> - )} - - - {({ goBack }) => ( -
-
Step 3: Review
- - -
- )} -
+