From 3ba46835c7718a26494108813aebf7706d8a196c Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:51:40 +0200 Subject: [PATCH 01/10] Updated Axelar USD (#3527) --- packages/web/e2e/tests/swap.stables.spec.ts | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/web/e2e/tests/swap.stables.spec.ts b/packages/web/e2e/tests/swap.stables.spec.ts index a31c97daf3..a8d1ebeec3 100644 --- a/packages/web/e2e/tests/swap.stables.spec.ts +++ b/packages/web/e2e/tests/swap.stables.spec.ts @@ -60,9 +60,9 @@ test.describe("Test Swap Stables feature", () => { await context.close(); }); - test("User should be able to swap USDC to USDC.axl", async () => { + test("User should be able to swap USDC to USDC.eth.axl", async () => { await swapPage.goto(); - await swapPage.selectPair("USDC", "USDC.axl"); + await swapPage.selectPair("USDC", "USDC.eth.axl"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); @@ -75,9 +75,9 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.getTransactionUrl()).toBeTruthy(); }); - test("User should be able to swap USDC.axl to USDC", async () => { + test("User should be able to swap USDC.eth.axl to USDC", async () => { await swapPage.goto(); - await swapPage.selectPair("USDC.axl", "USDC"); + await swapPage.selectPair("USDC.eth.axl", "USDC"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); @@ -90,9 +90,9 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.getTransactionUrl()).toBeTruthy(); }); - test("User should be able to swap USDT to USDT.axl", async () => { + test("User should be able to swap USDT to USDT.eth.axl", async () => { await swapPage.goto(); - await swapPage.selectPair("USDT", "USDT.axl"); + await swapPage.selectPair("USDT", "USDT.eth.axl"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); @@ -105,9 +105,9 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.getTransactionUrl()).toBeTruthy(); }); - test("User should be able to swap USDT.axl to USDT", async () => { + test("User should be able to swap USDT.eth.axl to USDT", async () => { await swapPage.goto(); - await swapPage.selectPair("USDT.axl", "USDT"); + await swapPage.selectPair("USDT.eth.axl", "USDT"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); @@ -120,9 +120,9 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.getTransactionUrl()).toBeTruthy(); }); - test("User should be able to swap USDT.axl to USDT.kava", async () => { + test("User should be able to swap USDT.eth.axl to USDT.kava", async () => { await swapPage.goto(); - await swapPage.selectPair("USDT.axl", "USDT.kava"); + await swapPage.selectPair("USDT.eth.axl", "USDT.kava"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); @@ -135,9 +135,9 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.getTransactionUrl()).toBeTruthy(); }); - test("User should be able to swap USDT.kava to USDT.axl", async () => { + test("User should be able to swap USDT.kava to USDT.eth.axl", async () => { await swapPage.goto(); - await swapPage.selectPair("USDT.kava", "USDT.axl"); + await swapPage.selectPair("USDT.kava", "USDT.eth.axl"); await swapPage.enterAmount("0.1"); await swapPage.showSwapInfo(); const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); From 9c14731b604da0cb3dceac3448a82636b55b75a7 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Tue, 16 Jul 2024 16:00:18 +0200 Subject: [PATCH 02/10] feat: aprs should be shown as a range (#3392) * feat: :sparkles: add queryPoolAprsRange service * feat: :sparkles: add new apr breakdown ui * test: :white_check_mark: update pool incentives * refactor: :recycle: remove max decimals * fix: :bug: fix build error * fix: :bug: fix build * feat: :sparkles: add apr range inside pools table * feat: :sparkles: remove legacy apr breakdown * feat: :lipstick: improve apr range style * fix: :bug: fix post merge * fix: :bug: fix apr null fallback * fix: :bug: fix table sorting --- .../complex/concentrated-liquidity/index.ts | 2 +- .../src/queries/complex/pools/bonding.ts | 13 ++- .../src/queries/complex/pools/incentives.ts | 106 +++++++++++++----- .../src/queries/data-services/pool-aprs.ts | 23 +++- packages/trpc/src/pools.ts | 2 +- .../web/components/cards/apr-breakdown.tsx | 102 ++++++++--------- .../components/complex/my-pools-card-grid.tsx | 9 +- .../web/components/complex/pools-table.tsx | 19 +++- .../components/pool-detail/concentrated.tsx | 24 +++- packages/web/components/pool-detail/share.tsx | 4 +- 10 files changed, 190 insertions(+), 114 deletions(-) diff --git a/packages/server/src/queries/complex/concentrated-liquidity/index.ts b/packages/server/src/queries/complex/concentrated-liquidity/index.ts index 517a96d72b..9893e0dc36 100644 --- a/packages/server/src/queries/complex/concentrated-liquidity/index.ts +++ b/packages/server/src/queries/complex/concentrated-liquidity/index.ts @@ -416,7 +416,7 @@ export async function mapGetUserPositionDetails({ const superfluidApr: RatePretty | undefined = ( await getPoolIncentives(pool.id) - )?.aprBreakdown?.superfluid; + )?.aprBreakdown?.superfluid?.upper; /** User's current superfluid delegation or undelegation */ let superfluidData: diff --git a/packages/server/src/queries/complex/pools/bonding.ts b/packages/server/src/queries/complex/pools/bonding.ts index 1e19421ff5..d972bb0eca 100644 --- a/packages/server/src/queries/complex/pools/bonding.ts +++ b/packages/server/src/queries/complex/pools/bonding.ts @@ -221,16 +221,16 @@ export async function getSharePoolBondDurations({ const incentivesBreakdown: BondDuration["incentivesBreakdown"] = []; if (isLongestDuration) { // internal mint incentives - if (poolIncentives?.aprBreakdown?.osmosis) { + if (poolIncentives?.aprBreakdown?.osmosis?.upper) { incentivesBreakdown.push({ - apr: poolIncentives?.aprBreakdown?.osmosis, + apr: poolIncentives?.aprBreakdown?.osmosis.upper, type: "osmosis", }); } // external incentives - if (poolIncentives?.aprBreakdown?.boost) { + if (poolIncentives?.aprBreakdown?.boost?.upper) { incentivesBreakdown.push({ - apr: poolIncentives?.aprBreakdown?.boost, + apr: poolIncentives?.aprBreakdown?.boost.upper, type: "boost", }); } @@ -240,7 +240,7 @@ export async function getSharePoolBondDurations({ new RatePretty(0) ); const swapFeeApr = - poolIncentives?.aprBreakdown?.swapFee ?? new RatePretty(0); + poolIncentives?.aprBreakdown?.swapFee?.upper ?? new RatePretty(0); aggregateApr = aggregateApr.add(swapFeeApr); // get superfluid info for this duration @@ -248,7 +248,8 @@ export async function getSharePoolBondDurations({ const userSyntheticLockIds: string[] = []; if (isSuperfluidDuration) { const superfluidApr = - poolIncentives?.aprBreakdown?.superfluid ?? new RatePretty(0); + poolIncentives?.aprBreakdown?.superfluid?.upper ?? + new RatePretty(0); aggregateApr = aggregateApr.add(superfluidApr); const userDelegations = bech32Address diff --git a/packages/server/src/queries/complex/pools/incentives.ts b/packages/server/src/queries/complex/pools/incentives.ts index 5128d2d80c..c550dd37d8 100644 --- a/packages/server/src/queries/complex/pools/incentives.ts +++ b/packages/server/src/queries/complex/pools/incentives.ts @@ -10,7 +10,10 @@ import { z } from "zod"; import { EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS } from "../../../env"; import { queryPriceRangeApr } from "../../../queries/data-services"; import { DEFAULT_LRU_OPTIONS } from "../../../utils/cache"; -import { queryPoolAprs } from "../../data-services/pool-aprs"; +import { + PoolDataRange, + queryPoolAprsRange, +} from "../../data-services/pool-aprs"; import { Gauge, queryGauges } from "../../osmosis"; import { Epochs } from "../../osmosis/epochs"; import { queryIncentivizedPools } from "../../osmosis/incentives/incentivized-pools"; @@ -35,12 +38,11 @@ export type PoolIncentiveType = (typeof allPoolIncentiveTypes)[number]; export type PoolIncentives = Partial<{ aprBreakdown: Partial<{ - total: RatePretty; - - swapFee: RatePretty; - superfluid: RatePretty; - osmosis: RatePretty; - boost: RatePretty; + total: PoolDataRange; + swapFee: PoolDataRange; + superfluid: PoolDataRange; + osmosis: PoolDataRange; + boost: PoolDataRange; }>; incentiveTypes: PoolIncentiveType[]; }>; @@ -95,42 +97,88 @@ export function getCachedPoolIncentivesMap(): Promise< key: "pools-incentives-map", ttl: 1000 * 30, // 30 seconds getFreshValue: async () => { - const aprs = await queryPoolAprs(); + const aprs = await queryPoolAprsRange(); return aprs.reduce((map, apr) => { - let total = maybeMakeRatePretty(apr.total_apr); - const swapFee = maybeMakeRatePretty(apr.swap_fees); - const superfluid = maybeMakeRatePretty(apr.superfluid); - const osmosis = maybeMakeRatePretty(apr.osmosis); - let boost = maybeMakeRatePretty(apr.boost); + let totalUpper = maybeMakeRatePretty(apr.total_apr.upper); + let totalLower = maybeMakeRatePretty(apr.total_apr.lower); + const swapFeeUpper = maybeMakeRatePretty(apr.swap_fees.upper); + const swapFeeLower = maybeMakeRatePretty(apr.swap_fees.lower); + const superfluidUpper = maybeMakeRatePretty(apr.superfluid.upper); + const superfluidLower = maybeMakeRatePretty(apr.superfluid.lower); + const osmosisUpper = maybeMakeRatePretty(apr.osmosis.upper); + const osmosisLower = maybeMakeRatePretty(apr.osmosis.lower); + let boostUpper = maybeMakeRatePretty(apr.boost.upper); + let boostLower = maybeMakeRatePretty(apr.boost.lower); // Temporarily exclude pools in this array from showing boost incentives given an issue on chain if ( ExcludedExternalBoostPools.includes(apr.pool_id) && - total && - boost + totalUpper && + totalLower && + boostUpper && + boostLower ) { - total = new RatePretty(total.toDec().sub(boost.toDec())); - boost = undefined; + totalUpper = new RatePretty( + totalUpper.toDec().sub(totalUpper.toDec()) + ); + totalLower = new RatePretty( + totalLower.toDec().sub(totalLower.toDec()) + ); + boostUpper = undefined; + boostLower = undefined; } // add list of incentives that are defined const incentiveTypes: PoolIncentiveType[] = []; - if (superfluid) incentiveTypes.push("superfluid"); - if (osmosis) incentiveTypes.push("osmosis"); - if (boost) incentiveTypes.push("boost"); - if (!superfluid && !osmosis && !boost) incentiveTypes.push("none"); + if (superfluidUpper && superfluidLower) + incentiveTypes.push("superfluid"); + if (osmosisUpper && osmosisLower) incentiveTypes.push("osmosis"); + if (boostUpper && osmosisLower) incentiveTypes.push("boost"); + if ( + !superfluidUpper && + !superfluidLower && + !osmosisUpper && + !osmosisLower && + !boostUpper && + !boostLower + ) + incentiveTypes.push("none"); const hasBreakdownData = - total || swapFee || superfluid || osmosis || boost; + totalUpper || + totalLower || + swapFeeUpper || + swapFeeLower || + superfluidUpper || + superfluidLower || + osmosisUpper || + osmosisLower || + boostUpper || + boostLower; map.set(apr.pool_id, { aprBreakdown: hasBreakdownData ? { - total, - swapFee, - superfluid, - osmosis, - boost, + total: { + upper: totalUpper, + lower: totalLower, + }, + swapFee: { + upper: swapFeeUpper, + lower: swapFeeLower, + }, + superfluid: { + upper: superfluidUpper, + lower: superfluidLower, + }, + osmosis: { + upper: osmosisUpper, + lower: osmosisLower, + }, + boost: { + upper: boostUpper, + lower: boostLower, + }, } : undefined, incentiveTypes, @@ -171,8 +219,8 @@ export function getConcentratedRangePoolApr({ } function maybeMakeRatePretty(value: number): RatePretty | undefined { - // numia will return 0 if the APR is not applicable, so return undefined to indicate that - if (value === 0) { + // numia will return 0 or null if the APR is not applicable, so return undefined to indicate that + if (value === 0 || value === null) { return undefined; } diff --git a/packages/server/src/queries/data-services/pool-aprs.ts b/packages/server/src/queries/data-services/pool-aprs.ts index 0dc19a6357..ad85e498cf 100644 --- a/packages/server/src/queries/data-services/pool-aprs.ts +++ b/packages/server/src/queries/data-services/pool-aprs.ts @@ -2,13 +2,13 @@ import { apiClient } from "@osmosis-labs/utils"; import { NUMIA_BASE_URL } from "../../env"; -type PoolApr = { +type PoolApr = { pool_id: string; - swap_fees: number; - superfluid: number; - osmosis: number; - boost: number; - total_apr: number; + swap_fees: T; + superfluid: T; + osmosis: T; + boost: T; + total_apr: T; }; /** Queries numia for a breakdown of APRs per pool. */ @@ -16,3 +16,14 @@ export function queryPoolAprs(): Promise { const url = new URL("/pools_apr", NUMIA_BASE_URL); return apiClient(url.toString()); } + +export type PoolDataRange = { + lower: T; + upper: T; +}; + +/** Queries numia for a breakdown of APRs per pool with range. */ +export function queryPoolAprsRange(): Promise[]> { + const url = new URL("/pools_apr_range", NUMIA_BASE_URL); + return apiClient(url.toString()); +} diff --git a/packages/trpc/src/pools.ts b/packages/trpc/src/pools.ts index 9baa873fbd..26ca186e90 100644 --- a/packages/trpc/src/pools.ts +++ b/packages/trpc/src/pools.ts @@ -32,7 +32,7 @@ const marketIncentivePoolsSortKeys = [ "feesSpent24hUsd", "volume7dUsd", "volume24hUsd", - "aprBreakdown.total", + "aprBreakdown.total.upper", ] as const; export type MarketIncentivePoolSortKey = (typeof marketIncentivePoolsSortKeys)[number]; diff --git a/packages/web/components/cards/apr-breakdown.tsx b/packages/web/components/cards/apr-breakdown.tsx index 780aa60bef..83dfcc7d00 100644 --- a/packages/web/components/cards/apr-breakdown.tsx +++ b/packages/web/components/cards/apr-breakdown.tsx @@ -1,57 +1,15 @@ import { RatePretty } from "@keplr-wallet/unit"; -import type { PoolIncentives } from "@osmosis-labs/server"; +import type { PoolDataRange, PoolIncentives } from "@osmosis-labs/server"; import classNames from "classnames"; -import { observer } from "mobx-react-lite"; import { FunctionComponent } from "react"; -import { EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS } from "~/config"; import { useTranslation } from "~/hooks"; -import { useStore } from "~/stores"; import { theme } from "~/tailwind.config"; import { Icon } from "../assets"; import { AprDisclaimerTooltip } from "../tooltip/apr-disclaimer"; import { CustomClasses } from "../types"; -/** - * Pools that are excluded from showing external boost incentives APRs. - */ -const ExcludedExternalBoostPools: string[] = - (EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS ?? []) as string[]; - -/** @deprecated uses Mobx query stores, do not use */ -export const AprBreakdownLegacy: FunctionComponent< - { poolId: string; showDisclaimerTooltip?: boolean } & CustomClasses -> = observer(({ poolId, className, showDisclaimerTooltip }) => { - const { queriesExternalStore } = useStore(); - const poolAprs = queriesExternalStore.queryPoolAprs.getForPool(poolId); - - let totalApr = poolAprs?.totalApr; - let boostApr = poolAprs?.boost; - - if ( - poolAprs?.poolId && - ExcludedExternalBoostPools.includes(poolAprs.poolId) && - totalApr && - boostApr - ) { - totalApr = new RatePretty(totalApr.toDec().sub(boostApr.toDec())); - boostApr = undefined; - } - - return ( - - ); -}); - export const AprBreakdown: FunctionComponent< PoolIncentives["aprBreakdown"] & CustomClasses & { showDisclaimerTooltip?: boolean } @@ -67,41 +25,57 @@ export const AprBreakdown: FunctionComponent< const { t } = useTranslation(); return ( -
+
- {swapFee && ( + {swapFee?.upper && swapFee?.lower && ( )} - {osmosis && ( + {osmosis?.upper && osmosis?.lower && (

OSMO {t("pools.aprBreakdown.boost")}

-

{osmosis.maxDecimals(1).toString()}

+ {osmosis.upper.maxDecimals(1).toString() === + osmosis.lower.maxDecimals(1).toString() ? ( +

{osmosis.upper.maxDecimals(1).toString()}

+ ) : ( +

+ {osmosis.lower.maxDecimals(1).toString()} -{" "} + {osmosis.upper.maxDecimals(1).toString()} +

+ )}
)} - {superfluid && ( + {superfluid?.upper && superfluid?.lower && ( )} - {boost && ( + {boost?.upper && boost?.lower && (

{t("pools.aprBreakdown.externalBoost")}

-

{boost.maxDecimals(1).toString()}

+ {boost.upper.maxDecimals(1).toString() === + boost.lower.maxDecimals(1).toString() ? ( +

{boost.upper.maxDecimals(1).toString()}

+ ) : ( +

+ {boost.lower.maxDecimals(1).toString()} -{" "} + {boost.upper.maxDecimals(1).toString()} +

+ )}
)}
- {total && ( + {total?.upper && total?.lower && (
{t("pools.aprBreakdown.total")}

)} -

{total.maxDecimals(1).toString()}

+ {total.upper.maxDecimals(1).toString() === + total.lower.maxDecimals(1).toString() ? ( +

{total.upper.maxDecimals(1).toString()}

+ ) : ( +

+ {total.lower.maxDecimals(1).toString()} -{" "} + {total.upper.maxDecimals(1).toString()} +

+ )}
)}
@@ -127,10 +109,20 @@ export const AprBreakdown: FunctionComponent< const BreakdownRow: FunctionComponent<{ label: string; - value: RatePretty; + value: PoolDataRange; }> = ({ label, value }) => (

{label}

-

{value.maxDecimals(1).toString()}

+ {value.lower?.maxDecimals(1).toString() === + value.upper?.maxDecimals(1).toString() ? ( +

+ {value.upper?.maxDecimals(1).toString()} +

+ ) : ( +

+ {value.lower?.maxDecimals(2).toString()} -{" "} + {value.upper?.maxDecimals(2).toString()} +

+ )}
); diff --git a/packages/web/components/complex/my-pools-card-grid.tsx b/packages/web/components/complex/my-pools-card-grid.tsx index a8051c9f9a..ba2e89e8e2 100644 --- a/packages/web/components/complex/my-pools-card-grid.tsx +++ b/packages/web/components/complex/my-pools-card-grid.tsx @@ -84,7 +84,10 @@ export const MyPoolsCardsGrid = observer(() => { ({ id, type, - apr = new RatePretty(0), + apr = { + lower: new RatePretty(0), + upper: new RatePretty(0), + }, poolLiquidity, userValue, reserveCoins, @@ -96,9 +99,9 @@ export const MyPoolsCardsGrid = observer(() => { { label: t("pools.APR"), value: isMobile ? ( - apr.maxDecimals(0).toString() + apr.upper?.maxDecimals(0).toString() ?? "" ) : ( -
{apr.maxDecimals(2).toString()}
+
{apr.upper?.maxDecimals(2).toString()}
), }, { diff --git a/packages/web/components/complex/pools-table.tsx b/packages/web/components/complex/pools-table.tsx index 04be19b4ba..a2f597b2a1 100644 --- a/packages/web/components/complex/pools-table.tsx +++ b/packages/web/components/complex/pools-table.tsx @@ -51,7 +51,7 @@ export const marketIncentivePoolsSortKeys = [ "feesSpent24hUsd", "volume7dUsd", "volume24hUsd", - "aprBreakdown.total", + "aprBreakdown.total.upper", ] as const; export type MarketIncentivePoolsSortKey = @@ -276,7 +276,7 @@ export const PoolsTable = (props: PropsWithChildren) => { header: () => ( - {aprBreakdown.boost || aprBreakdown.osmosis ? ( + {aprBreakdown.boost?.upper || aprBreakdown.osmosis?.upper ? (
) : ( )} - {aprBreakdown.total?.maxDecimals(0).toString() ?? ""} + {aprBreakdown?.total?.lower && + aprBreakdown?.total?.upper?.maxDecimals(1).toString() === + aprBreakdown?.total?.lower.maxDecimals(1).toString() ? ( +

{aprBreakdown?.total?.upper?.maxDecimals(1).toString()}

+ ) : ( +

+ {aprBreakdown?.total?.lower?.maxDecimals(1).toString()} -{" "} + {aprBreakdown?.total?.upper?.maxDecimals(1).toString()} +

+ )}

)) ?? diff --git a/packages/web/components/pool-detail/concentrated.tsx b/packages/web/components/pool-detail/concentrated.tsx index 270b484bb9..578eb5234a 100644 --- a/packages/web/components/pool-detail/concentrated.tsx +++ b/packages/web/components/pool-detail/concentrated.tsx @@ -31,7 +31,7 @@ import { formatPretty, getPriceExtendedFormatOptions } from "~/utils/formatter"; import { api } from "~/utils/trpc"; import { removeQueryParam } from "~/utils/url"; -import { AprBreakdownLegacy } from "../cards/apr-breakdown"; +import { AprBreakdown } from "../cards/apr-breakdown"; import { SkeletonLoader } from "../loaders/skeleton-loader"; const ConcentratedLiquidityDepthChart = dynamic( @@ -462,6 +462,16 @@ const UserAssetsAndExternalIncentives: FunctionComponent<{ poolId: string }> = const hasIncentives = concentratedPoolDetail.incentiveGauges.length > 0; + const { data: incentives, isLoading: isLoadingIncentives } = + api.edge.pools.getPoolIncentives.useQuery( + { + poolId, + }, + { + enabled: featureFlags.aprBreakdown, + } + ); + return (
@@ -503,11 +513,13 @@ const UserAssetsAndExternalIncentives: FunctionComponent<{ poolId: string }> =
{featureFlags.aprBreakdown && ( - + + + )} {hasIncentives && ( diff --git a/packages/web/components/pool-detail/share.tsx b/packages/web/components/pool-detail/share.tsx index 1d1c966231..ca883d20bf 100644 --- a/packages/web/components/pool-detail/share.tsx +++ b/packages/web/components/pool-detail/share.tsx @@ -593,8 +593,8 @@ export const SharePool: FunctionComponent<{ pool: Pool }> = observer( {isPoolIncentivesLoading ? ( ) : ( - poolIncentives?.aprBreakdown?.swapFee && ( -
{`${poolIncentives.aprBreakdown.swapFee + poolIncentives?.aprBreakdown?.swapFee?.upper && ( +
{`${poolIncentives.aprBreakdown.swapFee.upper .maxDecimals(2) .toString()} ${t("pool.APR")}`}
) From 04f113f80f88d0cc31f3851f60d637cb3464e507 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Tue, 16 Jul 2024 10:37:41 -0400 Subject: [PATCH 03/10] update portal URL (#3529) --- packages/bridge/src/wormhole/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bridge/src/wormhole/index.ts b/packages/bridge/src/wormhole/index.ts index b37e0775f3..8208ae2c95 100644 --- a/packages/bridge/src/wormhole/index.ts +++ b/packages/bridge/src/wormhole/index.ts @@ -63,20 +63,20 @@ export class WormholeBridgeProvider implements BridgeProvider { fromAsset, toAsset, }: GetBridgeExternalUrlParams): Promise { - // For now we use in-osmosis - const url = new URL("https://app.osmosis.zone/wormhole"); + // For now we use Portal Bridge + const url = new URL("https://portalbridge.com/"); url.searchParams.set( - "from", + "sourceChain", fromChain.chainName?.toLowerCase() ?? fromChain.chainId.toString() ); url.searchParams.set( - "to", + "targetChain", toChain.chainName?.toLowerCase() ?? toChain.chainId.toString() ); url.searchParams.set( - "token", - fromChain.chainType === "solana" ? fromAsset.denom : toAsset.denom + "asset", + fromChain.chainType === "solana" ? fromAsset.address : toAsset.address ); return { From cfd3a8b9067e531db3de923830f253c7fa6b205b Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Tue, 16 Jul 2024 10:57:44 -0400 Subject: [PATCH 04/10] fix padding (#3530) --- packages/web/components/bridge/immersive-bridge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/bridge/immersive-bridge.tsx b/packages/web/components/bridge/immersive-bridge.tsx index 56412e3cbe..269d133539 100644 --- a/packages/web/components/bridge/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive-bridge.tsx @@ -142,7 +142,7 @@ export const ImmersiveBridgeFlow = ({ /> )} -
+
Date: Tue, 16 Jul 2024 12:05:14 -0400 Subject: [PATCH 05/10] (1CT) Exclude other fee tokens for 1CT transactions (#3521) * fix: overspend matching * feat: exclude other fee tokens for 1CT transactions * feat: handle max transactions and include gas in our CI * feat: handle max amount queries * feat: streamline gas estimation * improvement: rename needed for tx --- .github/workflows/jest-tests.yml | 3 +- packages/stores/src/account/base.ts | 7 + packages/tx/src/__tests__/gas.spec.ts | 131 +----------------- packages/tx/src/gas.ts | 119 ++++++++-------- .../web/components/alert/tx-event-toast.ts | 6 +- packages/web/components/swap-tool/index.tsx | 3 +- packages/web/hooks/use-estimate-tx-fees.ts | 4 - packages/web/hooks/use-swap.tsx | 26 ++-- packages/web/localizations/de.json | 2 + packages/web/localizations/en.json | 2 + packages/web/localizations/es.json | 2 + packages/web/localizations/fa.json | 2 + packages/web/localizations/fr.json | 2 + packages/web/localizations/gu.json | 2 + packages/web/localizations/hi.json | 2 + packages/web/localizations/ja.json | 2 + packages/web/localizations/ko.json | 2 + packages/web/localizations/pl.json | 2 + packages/web/localizations/pt-br.json | 2 + packages/web/localizations/ro.json | 2 + packages/web/localizations/ru.json | 2 + packages/web/localizations/tr.json | 2 + packages/web/localizations/zh-cn.json | 2 + packages/web/localizations/zh-hk.json | 2 + packages/web/localizations/zh-tw.json | 2 + packages/web/pages/api/estimate-gas-fee.ts | 20 ++- 26 files changed, 143 insertions(+), 210 deletions(-) diff --git a/.github/workflows/jest-tests.yml b/.github/workflows/jest-tests.yml index ebfd019580..721d3bf816 100644 --- a/.github/workflows/jest-tests.yml +++ b/.github/workflows/jest-tests.yml @@ -8,7 +8,8 @@ jobs: strategy: matrix: node-version: [20.x] - package: ["web", "utils", "server", "stores", "pools", "math", "bridge"] + package: + ["web", "utils", "server", "stores", "pools", "math", "bridge", "tx"] steps: - name: Checkout Repo diff --git a/packages/stores/src/account/base.ts b/packages/stores/src/account/base.ts index 9ed761f7f1..c0a93aba2c 100644 --- a/packages/stores/src/account/base.ts +++ b/packages/stores/src/account/base.ts @@ -1280,6 +1280,13 @@ export class AccountStore[] = []> { nonCriticalExtensionOptions?.map(encodeAnyBase64), bech32Address: wallet.address, gasMultiplier: GasMultiplier, + } satisfies { + chainId: string; + messages: { typeUrl: string; value: string }[]; + nonCriticalExtensionOptions?: { typeUrl: string; value: string }[]; + bech32Address: string; + onlyDefaultFeeDenom?: boolean; + gasMultiplier: number; }, }); diff --git a/packages/tx/src/__tests__/gas.spec.ts b/packages/tx/src/__tests__/gas.spec.ts index 9d2046e916..e1b499be9e 100644 --- a/packages/tx/src/__tests__/gas.spec.ts +++ b/packages/tx/src/__tests__/gas.spec.ts @@ -570,7 +570,7 @@ describe("getGasFeeAmount", () => { expect(gasAmount.denom).toBe("uion"); expect(gasAmount.amount).toBe(expectedGasAmount); - expect(gasAmount.isNeededForTx).toBe(true); + expect(gasAmount.isSubtractiveFee).toBe(true); }); it("should return the correct gas amount with an alternative fee token when the last available fee token is not fully spent", async () => { @@ -657,7 +657,7 @@ describe("getGasFeeAmount", () => { expect(gasAmount.denom).toBe("uion"); expect(gasAmount.amount).toBe(expectedGasAmount); - expect(gasAmount.isNeededForTx).toBe(false); + expect(gasAmount.isSubtractiveFee).toBe(false); }); // Scenario: base fee token goes down in price and a very expensive (i.e. WBTC) alternative fee token is checked but resulting fee amount is <= 0 @@ -781,132 +781,7 @@ describe("getGasFeeAmount", () => { "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" ); expect(gasAmount.amount).toBe(expectedGasAmount); - expect(gasAmount.isNeededForTx).toBeUndefined(); - }); - - it("should return the correct gas amount with an alternative fee token that is the lesser of the spent amounts", async () => { - const gasLimit = 1000; - const chainId = "osmosis-1"; - const address = "osmo1..."; - const baseFee = 0.04655; - const altTokenSpotPrice = 1; - - (queryBalances as jest.Mock).mockResolvedValue({ - balances: [ - { - denom: "uosmo", - amount: "1", - }, - { - // ATOM - denom: - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - amount: "1000", - }, - { - denom: "uion", - amount: "1000000", - }, - ], - } as Awaited>); - (queryFeesBaseGasPrice as jest.Mock).mockResolvedValue({ - base_fee: baseFee.toString(), - } as Awaited>); - (queryFeeTokens as jest.Mock).mockResolvedValue({ - fee_tokens: [ - { - // ATOM - denom: - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - poolID: 1, - }, - { - denom: "uion", - poolID: 2, - }, - ], - } as Awaited>); - (queryFeesBaseDenom as jest.Mock).mockResolvedValue({ - base_denom: "uosmo", - } as Awaited>); - (queryFeeTokenSpotPrice as jest.Mock).mockImplementation(({ denom }) => { - // return the same spot price to isolate the differences in amounts - if ( - denom === "uion" || - denom === - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" - ) { - return Promise.resolve({ - pool_id: "2", - spot_price: altTokenSpotPrice.toString(), - } as Awaited>); - } - throw new Error("Mocked implementation got an unexpected fee denom"); - }); - - const gasMultiplier = 1.5; - const coinsSpent = [ - { denom: "uion", amount: "1000" }, - { - // notice smaller amount is second - denom: - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - amount: "100", - }, - ]; - - const gasAmount = ( - await getGasFeeAmount({ - chainId, - chainList: MockChains, - gasLimit: gasLimit.toString(), - bech32Address: address, - gasMultiplier, - coinsSpent, - }) - )[0]; - - const expectedGasAmount = new Dec(baseFee * gasMultiplier) - .quo(new Dec(altTokenSpotPrice)) - .mul(new Dec(1.01)) - .mul(new Dec(gasLimit)) - .truncate() - .toString(); - - expect(queryBalances).toBeCalledWith({ - chainId, - bech32Address: address, - chainList: MockChains, - }); - expect(queryFeesBaseGasPrice).toBeCalledWith({ - chainId, - chainList: MockChains, - }); - expect(queryFeeTokens).toBeCalledWith({ - chainId, - chainList: MockChains, - }); - expect(queryFeesBaseDenom).toBeCalledWith({ - chainId, - chainList: MockChains, - }); - expect(queryFeeTokenSpotPrice).toBeCalledWith({ - chainId, - chainList: MockChains, - denom: "uion", - }); - expect(queryFeeTokenSpotPrice).toBeCalledWith({ - chainId, - chainList: MockChains, - denom: - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - }); - - expect(gasAmount.denom).toBe( - "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" - ); - expect(gasAmount.amount).toBe(expectedGasAmount); - expect(gasAmount.isNeededForTx).toBe(false); + expect(gasAmount.isSubtractiveFee).toBe(false); }); it("should throw InsufficientFeeError when balance is insufficient without Osmosis fee module — no balances", async () => { diff --git a/packages/tx/src/gas.ts b/packages/tx/src/gas.ts index 4819c6f2eb..7953e466df 100644 --- a/packages/tx/src/gas.ts +++ b/packages/tx/src/gas.ts @@ -85,6 +85,8 @@ export async function estimateGasFee({ * Default: `1.5` */ gasMultiplier?: number; + sendCoin?: { denom: string; amount: string }; + /** Force the use of fee token returned by default from `getGasPrice`. Overrides `excludedFeeDenoms` option. */ onlyDefaultFeeDenom?: boolean; }): Promise { @@ -258,7 +260,7 @@ export async function getGasFeeAmount({ gasLimit, bech32Address, gasMultiplier = 1.5, - coinsSpent, + coinsSpent = [], }: { chainId: string; chainList: ChainWithFeatures[]; @@ -276,7 +278,7 @@ export async function getGasFeeAmount({ * spent by the given spent coins list. * Likely, the spent amount needs to be adjusted by subtracting this amount. */ - isNeededForTx?: boolean; + isSubtractiveFee?: boolean; }[] > { const chain = chainList.find((chain) => chain.chain_id === chainId); @@ -315,13 +317,21 @@ export async function getGasFeeAmount({ ); } - // first check unspent balances - const feeDenomsSpent = (coinsSpent ?? []).map(({ denom }) => denom); - const unspentFeeBalances = feeBalances.filter( - (balance) => !feeDenomsSpent.includes(balance.denom) - ); + /** + * Coins that can be subtracted to cover fee. + */ + let subtractiveFeeAmount: { + denom: string; + amount: string; + isSubtractiveFee: boolean; + }[] = []; + let alternativeFeeAmount: { + denom: string; + amount: string; + isSubtractiveFee: boolean; + }[] = []; - for (const { denom, amount } of unspentFeeBalances) { + for (const { amount, denom } of feeBalances) { const { gasPrice: feeDenomGasPrice } = await getGasPriceByFeeDenom({ chainId, chainList, @@ -340,70 +350,61 @@ export async function getGasFeeAmount({ ) continue; - // found enough to pay the fee that is not spent - return [ + const spentAmount = + coinsSpent.find((coinSpent) => coinSpent.denom === denom)?.amount || "0"; + /** + * If the spent amount (input amount in case of swap) is greater than the balance minus fees + * then we are missing balance to pay for the transaction. In this case, + * we need to find an alternative token or subtract this amount from the input. + */ + const isBalanceNeededForTx = new Dec(spentAmount).gt( + new Dec(amount).sub(new Dec(feeAmount)) + ); + + /** + * Following last comment, we now store a the subtractive coin that can be used in the transaction, + * as long as it's subtracted from the input. + */ + if (isBalanceNeededForTx) { + subtractiveFeeAmount = [ + { + amount: feeAmount, + denom, + isSubtractiveFee: true, + }, + ]; + continue; + } + + /** + * We will also store an alternative fee token. + * + * Useful to avoid leaving dust amounts for instances like a max amount swap. + */ + alternativeFeeAmount = [ { amount: feeAmount, denom, + isSubtractiveFee: false, }, ]; + break; } - // check spent balances last - if (!coinsSpent) + if (subtractiveFeeAmount.length === 0 && alternativeFeeAmount.length === 0) { throw new InsufficientFeeError( "Insufficient alternative balance for transaction fees. Please add funds to continue: " + bech32Address ); - const spentFeeBalances = feeBalances.filter((balance) => - coinsSpent.some(({ denom }) => denom === balance.denom) - ); - - // get all fee amounts for spent balances so we can prioritize the smallest amounts - const spentFeesWithAmounts = await Promise.all( - spentFeeBalances.map((spentFeeBalance) => - getGasPriceByFeeDenom({ - chainId, - chainList, - feeDenom: spentFeeBalance.denom, - gasMultiplier, - }).then(({ gasPrice }) => ({ - ...spentFeeBalance, - feeAmount: gasPrice.mul(new Dec(gasLimit)).truncate().toString(), - })) - ) - ); - - // filter spent fees by those that are not enough to pay the fee and sort by smallest amounts - const spentFees = spentFeesWithAmounts - .filter((spentFee) => - new Dec(spentFee.feeAmount).lte(new Dec(spentFee.amount)) - ) - .sort((a, b) => (new Int(a.feeAmount).lt(new Int(b.feeAmount)) ? -1 : 1)); - - for (const { amount, feeAmount, denom } of spentFees) { - // check for gas price conversion having too little precision - if (new Int(feeAmount).lte(new Int(1))) continue; - - const spentAmount = - coinsSpent.find((coinSpent) => coinSpent.denom === denom)?.amount || "0"; - const totalSpent = new Dec(spentAmount).add(new Dec(feeAmount)); - const isBalanceNeededForTx = totalSpent.gte(new Dec(amount)); - - return [ - { - amount: feeAmount, - denom, - isNeededForTx: isBalanceNeededForTx, - }, - ]; - // keep trying with other balances } - throw new InsufficientFeeError( - "Insufficient alternative balance for transaction fees. Please add funds to continue: " + - bech32Address - ); + /** + * we'll always prefer the alternative fee token against the balance needed for tx amount as we want to avoid + * having to subtract the input amount. Rather, we use the alternative fee token to avoid dust amounts. + */ + return alternativeFeeAmount.length > 0 + ? alternativeFeeAmount + : subtractiveFeeAmount; } /** diff --git a/packages/web/components/alert/tx-event-toast.ts b/packages/web/components/alert/tx-event-toast.ts index fde948aaba..ec719b640b 100644 --- a/packages/web/components/alert/tx-event-toast.ts +++ b/packages/web/components/alert/tx-event-toast.ts @@ -14,7 +14,6 @@ import { prettifyTxError } from "./prettify"; const txTimeoutHeightReachedErrorCode = 30; const BROADCASTING_TOAST_ID = "broadcast"; -export const BROADCASTING_FAILED_TOAST_ID = "broadcast-failed"; export function toastOnBroadcastFailed( getChain: (chainId: string) => ChainInfoInner @@ -33,10 +32,7 @@ export function toastOnBroadcastFailed( captionTranslationKey: prettifyTxError(caption, getChain(chainId).currencies) ?? caption, }, - ToastType.ERROR, - { - toastId: BROADCASTING_FAILED_TOAST_ID, - } + ToastType.ERROR ); }; } diff --git a/packages/web/components/swap-tool/index.tsx b/packages/web/components/swap-tool/index.tsx index df684e8faf..c8c6c4a18e 100644 --- a/packages/web/components/swap-tool/index.tsx +++ b/packages/web/components/swap-tool/index.tsx @@ -959,7 +959,8 @@ export const SwapTool: FunctionComponent = observer( !Boolean(swapState.quote) || isSwapToolLoading || Boolean(swapState.error) || - Boolean(swapState.networkFeeError))) + (Boolean(swapState.networkFeeError) && + !swapState.hasOverSpendLimitError))) } onClick={sendSwapTx} > diff --git a/packages/web/hooks/use-estimate-tx-fees.ts b/packages/web/hooks/use-estimate-tx-fees.ts index 1fa2ebba39..9a4ce3fc57 100644 --- a/packages/web/hooks/use-estimate-tx-fees.ts +++ b/packages/web/hooks/use-estimate-tx-fees.ts @@ -38,10 +38,6 @@ async function estimateTxFeesQueryFn({ accountStore: AccountStore<[OsmosisAccount, CosmosAccount, CosmwasmAccount]>; messages: EncodeObject[]; apiUtils: ReturnType; - sendToken?: { - balance: CoinPretty; - amount: CoinPretty; - }; signOptions?: SignOptions; }): Promise { if (!messages.length) throw new Error("No messages"); diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index b33e9db1a4..2c9605ceba 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -281,7 +281,7 @@ export function useSwap( })) : false; - const shouldBeSignedWithOneClickTrading = + let shouldBeSignedWithOneClickTrading = messageCanBeSignedWithOneClickTrading && !hasOverSpendLimitError && !networkFeeError; @@ -289,25 +289,35 @@ export function useSwap( if ( messageCanBeSignedWithOneClickTrading && !hasOverSpendLimitError && - networkFeeError + (networkFeeError || + networkFee?.amount.some(({ denom }) => denom !== "uosmo")) ) { try { - const ONE_CLICK_UNAVAILABLE_TOAST_ID = "ONE_CLICK_UNAVAILABLE"; + const TOAST_ID = networkFeeError + ? "ONE_CLICK_UNAVAILABLE" + : "ONE_CLICK_INSUFFICIENT_OSMO"; + const titleTranslationKey = networkFeeError + ? "oneClickTrading.toast.currentlyUnavailable" + : "oneClickTrading.toast.insufficientFunds"; + const buttonText = networkFeeError + ? "oneClickTrading.toast.approveManually" + : "oneClickTrading.toast.continueWithoutOneClickTrading"; + await new Promise((continueTx, reject) => { displayToast( { - titleTranslationKey: - "oneClickTrading.toast.currentlyUnavailable", + titleTranslationKey, captionElement: ( @@ -315,7 +325,7 @@ export function useSwap( }, ToastType.ONE_CLICK_TRADING, { - toastId: ONE_CLICK_UNAVAILABLE_TOAST_ID, + toastId: TOAST_ID, onClose: () => { reject(); }, diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index ca6c1364cf..01b1874d0d 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-Click-Handel abgelaufen", "oneClickTradingDisabled": "1-Click-Handel deaktiviert", "currentlyUnavailable": "1-Click Trading derzeit nicht verfügbar", + "insufficientFunds": "Unzureichende OSMO-Mittel für 1-Click-Trading", + "continueWithoutOneClickTrading": "Weiter ohne 1-Click Trading", "networkFeeTooHigh": "Netzwerkgebühr zu hoch", "startANewSession": "Neue Sitzung starten", "approveManually": "Manuell genehmigen in {walletName}", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 8a89784c11..e241224838 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-Click Trading expired", "oneClickTradingDisabled": "1-Click Trading disabled", "currentlyUnavailable": "1-Click Trading currently unavailable", + "insufficientFunds": "Insufficient OSMO funds for 1-Click Trading", + "continueWithoutOneClickTrading": "Continue without 1-Click Trading", "networkFeeTooHigh": "Network fee too high", "startANewSession": "Start a new session", "approveManually": "Approve manually in {walletName}", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 42ed7110f1..9e9832ee0a 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "El comercio con 1 clic expiró", "oneClickTradingDisabled": "Comercio con 1 clic deshabilitado", "currentlyUnavailable": "Trading con 1 clic no disponible actualmente", + "insufficientFunds": "Fondos OSMO insuficientes para el comercio con 1 clic", + "continueWithoutOneClickTrading": "Continuar sin operar con 1 clic", "networkFeeTooHigh": "Tarifa de red demasiado alta", "startANewSession": "Iniciar una nueva sesión", "approveManually": "Aprobar manualmente en {walletName}", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 10d8ed1fa0..d0edbcf70e 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-کلیک کنید تجارت منقضی شده است", "oneClickTradingDisabled": "1-کلیک Trading غیرفعال است", "currentlyUnavailable": "1-کلیک کنید تجارت در حال حاضر در دسترس نیست", + "insufficientFunds": "بودجه OSMO ناکافی برای تجارت 1 کلیکی", + "continueWithoutOneClickTrading": "بدون معامله 1 کلیکی ادامه دهید", "networkFeeTooHigh": "هزینه شبکه خیلی بالاست", "startANewSession": "یک جلسه جدید را شروع کنید", "approveManually": "تأیید دستی در {walletName}", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 42a821d49e..5a709f2742 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "Le trading en 1 clic a expiré", "oneClickTradingDisabled": "Trading en 1 clic désactivé", "currentlyUnavailable": "Trading en 1 clic actuellement indisponible", + "insufficientFunds": "Fonds OSMO insuffisants pour le trading en 1 clic", + "continueWithoutOneClickTrading": "Continuer sans trading en 1 clic", "networkFeeTooHigh": "Frais de réseau trop élevés", "startANewSession": "Démarrer une nouvelle session", "approveManually": "Approuver manuellement dans {walletName}", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 0dd4e84927..82df28d61d 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-ક્લિક કરો ટ્રેડિંગ સમાપ્ત", "oneClickTradingDisabled": "1-ક્લિક કરો ટ્રેડિંગ અક્ષમ", "currentlyUnavailable": "1-ક્લિક ટ્રેડિંગ હાલમાં અનુપલબ્ધ છે", + "insufficientFunds": "1-ક્લિક ટ્રેડિંગ માટે અપર્યાપ્ત OSMO ભંડોળ", + "continueWithoutOneClickTrading": "1-ક્લિક ટ્રેડિંગ વિના ચાલુ રાખો", "networkFeeTooHigh": "નેટવર્ક ફી ખૂબ વધારે છે", "startANewSession": "નવું સત્ર શરૂ કરો", "approveManually": "{walletName} માં મેન્યુઅલી મંજૂર કરો", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 7e90085b50..eaa8c726e5 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-क्लिक ट्रेडिंग समाप्त हो गई", "oneClickTradingDisabled": "1-क्लिक ट्रेडिंग अक्षम", "currentlyUnavailable": "1-क्लिक ट्रेडिंग फिलहाल उपलब्ध नहीं है", + "insufficientFunds": "1-क्लिक ट्रेडिंग के लिए OSMO फंड अपर्याप्त है", + "continueWithoutOneClickTrading": "1-क्लिक ट्रेडिंग के बिना जारी रखें", "networkFeeTooHigh": "नेटवर्क शुल्क बहुत अधिक है", "startANewSession": "नया सत्र शुरू करें", "approveManually": "{walletName} में मैन्युअल रूप से स्वीकृत करें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 8ba2a869cb..bf5119401d 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-Click 取引の有効期限が切れました", "oneClickTradingDisabled": "1-Click 取引が無効になっています", "currentlyUnavailable": "1クリック取引は現在利用できません", + "insufficientFunds": "ワンクリック取引に必要なOSMO資金が不足しています", + "continueWithoutOneClickTrading": "1クリック取引なしで続行", "networkFeeTooHigh": "ネットワーク料金が高すぎる", "startANewSession": "新しいセッションを開始する", "approveManually": "{walletName}で手動で承認する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 4069db439f..afbed15780 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "1-클릭 거래가 만료되었습니다", "oneClickTradingDisabled": "1-클릭 거래 비활성화됨", "currentlyUnavailable": "현재 1-클릭 거래를 이용할 수 없습니다", + "insufficientFunds": "1-Click 거래를 위한 OSMO 자금이 부족합니다.", + "continueWithoutOneClickTrading": "원클릭 거래 없이 계속하기", "networkFeeTooHigh": "네트워크 수수료가 너무 높음", "startANewSession": "새 세션 시작", "approveManually": "{walletName} 에서 수동으로 승인하세요.", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 23deb0eac2..e430ad02bd 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "Transakcja 1-Click wygasła", "oneClickTradingDisabled": "Handel jednym kliknięciem wyłączony", "currentlyUnavailable": "Handel jednym kliknięciem jest obecnie niedostępny", + "insufficientFunds": "Niewystarczające środki OSMO do handlu jednym kliknięciem", + "continueWithoutOneClickTrading": "Kontynuuj bez handlu jednym kliknięciem", "networkFeeTooHigh": "Opłata sieciowa jest zbyt wysoka", "startANewSession": "Rozpocznij nową sesję", "approveManually": "Zatwierdź ręcznie w {walletName}", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index deaba8b1e1..ad840cd3f5 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "A negociação em 1 clique expirou", "oneClickTradingDisabled": "Negociação em 1 Clique desativada", "currentlyUnavailable": "Negociação em 1 Clique atualmente indisponível", + "insufficientFunds": "Fundos OSMO insuficientes para negociação em 1 clique", + "continueWithoutOneClickTrading": "Continue sem negociação em 1 clique", "networkFeeTooHigh": "Taxa de rede muito alta", "startANewSession": "Iniciar uma nova sessão", "approveManually": "Aprovar manualmente em {walletName}", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 14a6f41851..ada4c58049 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "Tranzacționarea cu 1 clic a expirat", "oneClickTradingDisabled": "1-Click Trading este dezactivat", "currentlyUnavailable": "1-Click Trading nu este disponibil momentan", + "insufficientFunds": "Fonduri OSMO insuficiente pentru tranzacționarea cu 1 clic", + "continueWithoutOneClickTrading": "Continuați fără tranzacționare cu 1 clic", "networkFeeTooHigh": "Taxa de rețea prea mare", "startANewSession": "Începeți o nouă sesiune", "approveManually": "Aprobați manual în {walletName}", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index e37bb6f182..09807b0c5b 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "Срок действия 1-Click Trading истек", "oneClickTradingDisabled": "Торговля в 1 клик отключена", "currentlyUnavailable": "Торговля в 1 клик в настоящее время недоступна", + "insufficientFunds": "Недостаточно средств OSMO для торговли в 1 клик", + "continueWithoutOneClickTrading": "Продолжить без торговли в 1 клик", "networkFeeTooHigh": "Плата за сеть слишком высока", "startANewSession": "Начать новый сеанс", "approveManually": "Утвердить вручную в {walletName}", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 4c86bad186..0627cf8cea 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "Tek Tıklamayla Ticaretin süresi doldu", "oneClickTradingDisabled": "Tek Tıklamayla Ticaret devre dışı bırakıldı", "currentlyUnavailable": "Tek Tıkla Ticaret şu anda kullanılamıyor", + "insufficientFunds": "Tek Tıklamayla Ticaret için yetersiz OSMO fonu", + "continueWithoutOneClickTrading": "Tek Tıklamayla İşlem Yapmadan Devam Edin", "networkFeeTooHigh": "Ağ ücreti çok yüksek", "startANewSession": "Yeni bir oturum başlat", "approveManually": "{walletName} içinde manuel olarak onaylayın", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index e5c68d87d8..00dcbc109f 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "一键交易已过期", "oneClickTradingDisabled": "一键交易已禁用", "currentlyUnavailable": "一键交易目前不可用", + "insufficientFunds": "OSMO 资金不足,无法进行一键交易", + "continueWithoutOneClickTrading": "无需一键交易即可继续", "networkFeeTooHigh": "网络费太高", "startANewSession": "开始新会话", "approveManually": "在{walletName}中手动批准", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 89e49f2321..b04383be52 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "一鍵交易已過期", "oneClickTradingDisabled": "一鍵交易已停用", "currentlyUnavailable": "一鍵交易目前無法使用", + "insufficientFunds": "OSMO 資金不足,無法進行一鍵交易", + "continueWithoutOneClickTrading": "無需一鍵交易即可繼續", "networkFeeTooHigh": "網路費太高", "startANewSession": "開始新會話", "approveManually": "在{walletName}中手動核准", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 72868eadc9..c3affe7dd6 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -496,6 +496,8 @@ "oneClickTradingExpired": "一鍵交易已過期", "oneClickTradingDisabled": "一鍵交易已停用", "currentlyUnavailable": "一鍵交易目前無法使用", + "insufficientFunds": "OSMO 資金不足,無法進行一鍵交易", + "continueWithoutOneClickTrading": "無需一鍵交易即可繼續", "networkFeeTooHigh": "網路費太高", "startANewSession": "開始新會話", "approveManually": "在{walletName}中手動核准", diff --git a/packages/web/pages/api/estimate-gas-fee.ts b/packages/web/pages/api/estimate-gas-fee.ts index aae6b9dd75..49801b5baa 100644 --- a/packages/web/pages/api/estimate-gas-fee.ts +++ b/packages/web/pages/api/estimate-gas-fee.ts @@ -3,6 +3,7 @@ import { estimateGasFee, SimulateNotAvailableError, } from "@osmosis-labs/tx"; +import { ApiClientError } from "@osmosis-labs/utils"; import { NextApiRequest, NextApiResponse } from "next"; import { ChainList } from "~/config/generated/chain-list"; @@ -33,13 +34,14 @@ export default async function handler( bech32Address, onlyDefaultFeeDenom, gasMultiplier, + sendCoin, } = req.body as { chainId: string; messages: { typeUrl: string; value: string }[]; nonCriticalExtensionOptions?: { typeUrl: string; value: string }[]; bech32Address: string; - excludedFeeMinimalDenoms?: string[]; - onlyDefaultFeeDenom: boolean; + sendCoin?: { denom: string; amount: string }; + onlyDefaultFeeDenom?: boolean; gasMultiplier: number; }; @@ -55,11 +57,21 @@ export default async function handler( }, onlyDefaultFeeDenom, gasMultiplier, + sendCoin, }); return res.status(200).json(gasFee); } catch (e) { - if (e instanceof SimulateNotAvailableError) { - return res.status(400).json({ message: e.message }); + const error = e as Error | SimulateNotAvailableError | ApiClientError; + if (error instanceof SimulateNotAvailableError) { + return res.status(400).json({ message: error.message }); + } + + /** + * It's a cosmos node error. Forward data as 200 to the client. + */ + if (error instanceof ApiClientError && error.data?.code) { + console.log("errorr!", error); + return res.status(500).json(error.data); } return res.status(500).json({ error: e instanceof Error ? e.message : e }); From 59d4fcec73c9057f1af94af02a15b5d0a6d14a86 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Tue, 16 Jul 2024 16:20:46 -0400 Subject: [PATCH 06/10] (Deposit/Withdraw) Cleanup: move bridge specific errors into providers (#3522) * handle errors in providers * cleanup * catch insufficient fee * comment --- packages/bridge/src/axelar/index.ts | 17 +++++- packages/bridge/src/ibc/index.ts | 15 +++++ packages/bridge/src/skip/index.ts | 59 ++++++++++++++++--- .../components/bridge/use-bridge-quotes.ts | 27 +-------- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index 79480caefb..6a7f2f8eb3 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -170,7 +170,7 @@ export class AxelarBridgeProvider implements BridgeProvider { ) { throw new BridgeQuoteError({ bridgeId: AxelarBridgeProvider.ID, - errorType: "UnsupportedQuoteError", + errorType: "InsufficientAmountError", message: `Negative output amount ${new IntPretty( expectedOutputAmount ).trim(true)} for asset in: ${new IntPretty(fromAmount).trim( @@ -413,6 +413,21 @@ export class AxelarBridgeProvider implements BridgeProvider { ], }, bech32Address: params.fromAddress, + }).catch((e) => { + if ( + e instanceof Error && + e.message.includes( + "No fee tokens found with sufficient balance on account" + ) + ) { + throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, + errorType: "InsufficientAmountError", + message: e.message, + }); + } + + throw e; }); const gasFee = txSimulation.amount[0]; diff --git a/packages/bridge/src/ibc/index.ts b/packages/bridge/src/ibc/index.ts index dc13359301..fc0a63f21c 100644 --- a/packages/bridge/src/ibc/index.ts +++ b/packages/bridge/src/ibc/index.ts @@ -63,6 +63,21 @@ export class IbcBridgeProvider implements BridgeProvider { ], }, bech32Address: params.fromAddress, + }).catch((e) => { + if ( + e instanceof Error && + e.message.includes( + "No fee tokens found with sufficient balance on account" + ) + ) { + throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, + errorType: "InsufficientAmountError", + message: e.message, + }); + } + + throw e; }); const gasFee = txSimulation.amount[0]; const gasAsset = this.getGasAsset(fromChainId, gasFee.denom); diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index a7063cefee..42a0382203 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -112,13 +112,43 @@ export class SkipBridgeProvider implements BridgeProvider { }); } - const route = await this.skipClient.route({ - source_asset_denom: sourceAsset.denom, - source_asset_chain_id: fromChain.chainId.toString(), - dest_asset_denom: destinationAsset.denom, - dest_asset_chain_id: toChain.chainId.toString(), - amount_in: fromAmount, - }); + const route = await this.skipClient + .route({ + source_asset_denom: sourceAsset.denom, + source_asset_chain_id: fromChain.chainId.toString(), + dest_asset_denom: destinationAsset.denom, + dest_asset_chain_id: toChain.chainId.toString(), + amount_in: fromAmount, + }) + .catch((e) => { + if (e instanceof Error) { + const msg = e.message; + if ( + msg.includes( + "Input amount is too low to cover" + // Could be Axelar or CCTP + ) + ) { + throw new BridgeQuoteError({ + bridgeId: SkipBridgeProvider.ID, + errorType: "InsufficientAmountError", + message: msg, + }); + } + if ( + msg.includes( + "cannot transfer across cctp after route demands swap" + ) + ) { + throw new BridgeQuoteError({ + bridgeId: SkipBridgeProvider.ID, + errorType: "NoQuotesError", + message: msg, + }); + } + } + throw e; + }); const addressList = await this.getAddressList( route.chain_ids, @@ -818,6 +848,21 @@ export class SkipBridgeProvider implements BridgeProvider { ], }, bech32Address: params.fromAddress, + }).catch((e) => { + if ( + e instanceof Error && + e.message.includes( + "No fee tokens found with sufficient balance on account" + ) + ) { + throw new BridgeQuoteError({ + bridgeId: SkipBridgeProvider.ID, + errorType: "InsufficientAmountError", + message: e.message, + }); + } + + throw e; }); const gasFee = txSimulation.amount[0]; diff --git a/packages/web/components/bridge/use-bridge-quotes.ts b/packages/web/components/bridge/use-bridge-quotes.ts index 187cabcb05..460dfd053a 100644 --- a/packages/web/components/bridge/use-bridge-quotes.ts +++ b/packages/web/components/bridge/use-bridge-quotes.ts @@ -360,17 +360,7 @@ export const useBridgeQuotes = ({ ]); const isInsufficientFee = useMemo(() => { - if ( - someError?.message.includes( - "No fee tokens found with sufficient balance on account" - ) || - someError?.message.includes( - "Input amount is too low to cover CCTP bridge relay fee" - ) || - someError?.message.includes( - "cannot transfer across cctp after route demands swap" - ) - ) + if (someError?.message.includes("InsufficientAmountError" as BridgeError)) return true; if (!inputCoin || !selectedQuote || !selectedQuote.gasCost) return false; @@ -401,21 +391,6 @@ export const useBridgeQuotes = ({ return false; }, [someError, inputCoin, selectedQuote]); - // const isInsufficientFee = - // // Cosmos not fee tokens error - // someError?.message.includes( - // "No fee tokens found with sufficient balance on account" - // ) || - // (inputAmountRaw !== "" && - // availableBalance && - // selectedQuote?.gasCost !== undefined && - // selectedQuote.gasCost.denom === availableBalance.denom && // make sure the fee is in the same denom as the asset - // inputCoin - // ?.toDec() - // .sub(availableBalance.toDec()) // subtract by available balance to get the maximum transfer amount - // .abs() - // .lt(selectedQuote.gasCost.toDec())); - const bridgeTransaction = api.bridgeTransfer.getTransactionRequestByBridge.useQuery( { From f4c52fa179aa487f47af9bb2e20c744f5f4c7d54 Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:20:09 +0200 Subject: [PATCH 07/10] Fixed Portfolio and Trnasaction tests (#3537) --- .github/workflows/monitoring-e2e-tests.yml | 2 +- packages/web/e2e/pages/portfolio-page.ts | 12 +----------- packages/web/e2e/pages/transactions-page.ts | 5 +++++ packages/web/e2e/tests/portfolio.wallet.spec.ts | 8 +++++--- packages/web/e2e/tests/transactions.wallet.spec.ts | 14 ++++++++------ 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/monitoring-e2e-tests.yml b/.github/workflows/monitoring-e2e-tests.yml index f02df28070..fcc4b8bd47 100644 --- a/.github/workflows/monitoring-e2e-tests.yml +++ b/.github/workflows/monitoring-e2e-tests.yml @@ -16,7 +16,7 @@ jobs: echo "matrix={\"include\":[{ \"base-url\":\"https://app.osmosis.zone\", \"server-url\":\"https://sqs.osmosis.zone\", \"env\": \"production\", \"timeseries-url\":\"https://stage-proxy-data-api.osmosis-labs.workers.dev\"}, { \"base-url\":\"https://stage.osmosis.zone\", \"server-url\":\"https://sqs.stage.osmosis.zone\", \"env\": \"staging\", \"timeseries-url\":\"https://stage-proxy-data-api.osmosis-labs.workers.dev\"}]}" >> "$GITHUB_OUTPUT" frontend-e2e-tests: - name: production + name: production-swap runs-on: macos-latest environment: name: prod_swap_test diff --git a/packages/web/e2e/pages/portfolio-page.ts b/packages/web/e2e/pages/portfolio-page.ts index f4f130047a..7a9cec822d 100644 --- a/packages/web/e2e/pages/portfolio-page.ts +++ b/packages/web/e2e/pages/portfolio-page.ts @@ -31,19 +31,9 @@ export class PortfolioPage extends BasePage { console.log("FE opened at: " + currentUrl); } - async hideZeroBalances() { - await this.hideZeros.click(); - await this.page.waitForTimeout(1000); - } - - async viewMoreBalances() { - await this.viewMore.click(); - await this.page.waitForTimeout(1000); - } - async getBalanceFor(token: string) { const bal = this.page - .locator(`//tbody/tr//a[@href="/assets/${token}"]`) + .locator(`//tbody/tr//a[@href="/assets/${token}?ref=portfolio"]`) .nth(1); let tokenBalance: string = await bal.innerText(); console.log(`Balance for ${token}: ${tokenBalance}`); diff --git a/packages/web/e2e/pages/transactions-page.ts b/packages/web/e2e/pages/transactions-page.ts index 19da7126a5..3ce069878f 100644 --- a/packages/web/e2e/pages/transactions-page.ts +++ b/packages/web/e2e/pages/transactions-page.ts @@ -15,6 +15,11 @@ export class TransactionsPage extends BasePage { this.closeTransactionBtn = page.getByLabel("Close").nth(1); } + async open() { + await this.page.goto("/transactions"); + return this; + } + async viewTransactionByNumber(number: number) { await this.transactionRow.nth(number).click(); await this.page.waitForTimeout(1000); diff --git a/packages/web/e2e/tests/portfolio.wallet.spec.ts b/packages/web/e2e/tests/portfolio.wallet.spec.ts index 24f9439aef..0a78bc81c9 100644 --- a/packages/web/e2e/tests/portfolio.wallet.spec.ts +++ b/packages/web/e2e/tests/portfolio.wallet.spec.ts @@ -10,7 +10,7 @@ import { WalletPage } from "../pages/wallet-page"; test.describe("Test Portfolio feature", () => { let context: BrowserContext; - const privateKey = process.env.PRIVATE_KEY ?? "private_key"; + const privateKey = process.env.PRIVATE_KEY ?? "pk"; const password = process.env.PASSWORD ?? "TestPassword2024."; let portfolioPage: PortfolioPage; let dollarBalanceRegEx = /\$\d+/; @@ -49,8 +49,6 @@ test.describe("Test Portfolio feature", () => { portfolioPage = new PortfolioPage(page); await portfolioPage.goto(); await portfolioPage.connectWallet(); - await portfolioPage.hideZeroBalances(); - await portfolioPage.viewMoreBalances(); }); test.afterAll(async () => { @@ -85,6 +83,10 @@ test.describe("Test Portfolio feature", () => { expect(ethBalance).toMatch(dollarBalanceRegEx); const kujiBalance = await portfolioPage.getBalanceFor("KUJI"); expect(kujiBalance).toMatch(dollarBalanceRegEx); + const solBalance = await portfolioPage.getBalanceFor("SOL"); + expect(solBalance).toMatch(dollarBalanceRegEx); + const milkTIABalance = await portfolioPage.getBalanceFor("milkTIA"); + expect(milkTIABalance).toMatch(dollarBalanceRegEx); const abtcBalance = await portfolioPage.getBalanceFor("allBTC"); // allBTC has not $ price atm expect(abtcBalance).toMatch(digitBalanceRegEx); diff --git a/packages/web/e2e/tests/transactions.wallet.spec.ts b/packages/web/e2e/tests/transactions.wallet.spec.ts index 9f95fab2c4..fc379143b6 100644 --- a/packages/web/e2e/tests/transactions.wallet.spec.ts +++ b/packages/web/e2e/tests/transactions.wallet.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { BrowserContext, chromium, expect, test } from "@playwright/test"; +import { BrowserContext, chromium, expect, Page, test } from "@playwright/test"; import process from "process"; import { SwapPage } from "~/e2e/pages/swap-page"; @@ -11,9 +11,10 @@ import { WalletPage } from "../pages/wallet-page"; test.describe("Test Transactions feature", () => { let context: BrowserContext; + let page: Page; const walletId = process.env.WALLET_ID ?? "osmo1ka7q9tykdundaanr07taz3zpt5k72c0ut5r4xa"; - const privateKey = process.env.PRIVATE_KEY ?? "private_key"; + const privateKey = process.env.PRIVATE_KEY ?? "pk"; const password = process.env.PASSWORD ?? "TestPassword2024."; let portfolioPage: PortfolioPage; let transactionsPage: TransactionsPage; @@ -36,7 +37,7 @@ test.describe("Test Transactions feature", () => { // Get all new pages (including Extension) in the context and wait const emptyPage = context.pages()[0]; await emptyPage.waitForTimeout(2000); - const page = context.pages()[1]; + page = context.pages()[1]; const walletPage = new WalletPage(page); // Import existing Wallet (could be aggregated in one function). await walletPage.importWalletWithPrivateKey(privateKey); @@ -44,10 +45,11 @@ test.describe("Test Transactions feature", () => { await walletPage.selectChainsAndSave(); await walletPage.finish(); // Switch to Application - portfolioPage = new PortfolioPage(context.pages()[0]); + page = context.pages()[0]; + portfolioPage = new PortfolioPage(page); await portfolioPage.goto(); await portfolioPage.connectWallet(); - transactionsPage = await portfolioPage.viewTransactionsPage(); + transactionsPage = await new TransactionsPage(page).open(); }); test.afterAll(async () => { @@ -66,7 +68,7 @@ test.describe("Test Transactions feature", () => { await transactionsPage.closeTransaction(); }); - test("User should be able to see a new transaction", async () => { + test.skip("User should be able to see a new transaction", async () => { swapPage = new SwapPage(context.pages()[0]); await swapPage.goto(); await swapPage.selectPair("USDC", "USDT"); From 9d3acb37f52577e994bae46dacecc50f1ca0ff0a Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Wed, 17 Jul 2024 15:54:28 +0200 Subject: [PATCH 08/10] Revert "feat: aprs should be shown as a range (#3392)" (#3539) This reverts commit 9c14731b604da0cb3dceac3448a82636b55b75a7. --- .../complex/concentrated-liquidity/index.ts | 2 +- .../src/queries/complex/pools/bonding.ts | 13 +-- .../src/queries/complex/pools/incentives.ts | 106 +++++------------- .../src/queries/data-services/pool-aprs.ts | 23 +--- packages/trpc/src/pools.ts | 2 +- .../web/components/cards/apr-breakdown.tsx | 102 +++++++++-------- .../components/complex/my-pools-card-grid.tsx | 9 +- .../web/components/complex/pools-table.tsx | 19 +--- .../components/pool-detail/concentrated.tsx | 24 +--- packages/web/components/pool-detail/share.tsx | 4 +- 10 files changed, 114 insertions(+), 190 deletions(-) diff --git a/packages/server/src/queries/complex/concentrated-liquidity/index.ts b/packages/server/src/queries/complex/concentrated-liquidity/index.ts index 9893e0dc36..517a96d72b 100644 --- a/packages/server/src/queries/complex/concentrated-liquidity/index.ts +++ b/packages/server/src/queries/complex/concentrated-liquidity/index.ts @@ -416,7 +416,7 @@ export async function mapGetUserPositionDetails({ const superfluidApr: RatePretty | undefined = ( await getPoolIncentives(pool.id) - )?.aprBreakdown?.superfluid?.upper; + )?.aprBreakdown?.superfluid; /** User's current superfluid delegation or undelegation */ let superfluidData: diff --git a/packages/server/src/queries/complex/pools/bonding.ts b/packages/server/src/queries/complex/pools/bonding.ts index d972bb0eca..1e19421ff5 100644 --- a/packages/server/src/queries/complex/pools/bonding.ts +++ b/packages/server/src/queries/complex/pools/bonding.ts @@ -221,16 +221,16 @@ export async function getSharePoolBondDurations({ const incentivesBreakdown: BondDuration["incentivesBreakdown"] = []; if (isLongestDuration) { // internal mint incentives - if (poolIncentives?.aprBreakdown?.osmosis?.upper) { + if (poolIncentives?.aprBreakdown?.osmosis) { incentivesBreakdown.push({ - apr: poolIncentives?.aprBreakdown?.osmosis.upper, + apr: poolIncentives?.aprBreakdown?.osmosis, type: "osmosis", }); } // external incentives - if (poolIncentives?.aprBreakdown?.boost?.upper) { + if (poolIncentives?.aprBreakdown?.boost) { incentivesBreakdown.push({ - apr: poolIncentives?.aprBreakdown?.boost.upper, + apr: poolIncentives?.aprBreakdown?.boost, type: "boost", }); } @@ -240,7 +240,7 @@ export async function getSharePoolBondDurations({ new RatePretty(0) ); const swapFeeApr = - poolIncentives?.aprBreakdown?.swapFee?.upper ?? new RatePretty(0); + poolIncentives?.aprBreakdown?.swapFee ?? new RatePretty(0); aggregateApr = aggregateApr.add(swapFeeApr); // get superfluid info for this duration @@ -248,8 +248,7 @@ export async function getSharePoolBondDurations({ const userSyntheticLockIds: string[] = []; if (isSuperfluidDuration) { const superfluidApr = - poolIncentives?.aprBreakdown?.superfluid?.upper ?? - new RatePretty(0); + poolIncentives?.aprBreakdown?.superfluid ?? new RatePretty(0); aggregateApr = aggregateApr.add(superfluidApr); const userDelegations = bech32Address diff --git a/packages/server/src/queries/complex/pools/incentives.ts b/packages/server/src/queries/complex/pools/incentives.ts index c550dd37d8..5128d2d80c 100644 --- a/packages/server/src/queries/complex/pools/incentives.ts +++ b/packages/server/src/queries/complex/pools/incentives.ts @@ -10,10 +10,7 @@ import { z } from "zod"; import { EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS } from "../../../env"; import { queryPriceRangeApr } from "../../../queries/data-services"; import { DEFAULT_LRU_OPTIONS } from "../../../utils/cache"; -import { - PoolDataRange, - queryPoolAprsRange, -} from "../../data-services/pool-aprs"; +import { queryPoolAprs } from "../../data-services/pool-aprs"; import { Gauge, queryGauges } from "../../osmosis"; import { Epochs } from "../../osmosis/epochs"; import { queryIncentivizedPools } from "../../osmosis/incentives/incentivized-pools"; @@ -38,11 +35,12 @@ export type PoolIncentiveType = (typeof allPoolIncentiveTypes)[number]; export type PoolIncentives = Partial<{ aprBreakdown: Partial<{ - total: PoolDataRange; - swapFee: PoolDataRange; - superfluid: PoolDataRange; - osmosis: PoolDataRange; - boost: PoolDataRange; + total: RatePretty; + + swapFee: RatePretty; + superfluid: RatePretty; + osmosis: RatePretty; + boost: RatePretty; }>; incentiveTypes: PoolIncentiveType[]; }>; @@ -97,88 +95,42 @@ export function getCachedPoolIncentivesMap(): Promise< key: "pools-incentives-map", ttl: 1000 * 30, // 30 seconds getFreshValue: async () => { - const aprs = await queryPoolAprsRange(); + const aprs = await queryPoolAprs(); return aprs.reduce((map, apr) => { - let totalUpper = maybeMakeRatePretty(apr.total_apr.upper); - let totalLower = maybeMakeRatePretty(apr.total_apr.lower); - const swapFeeUpper = maybeMakeRatePretty(apr.swap_fees.upper); - const swapFeeLower = maybeMakeRatePretty(apr.swap_fees.lower); - const superfluidUpper = maybeMakeRatePretty(apr.superfluid.upper); - const superfluidLower = maybeMakeRatePretty(apr.superfluid.lower); - const osmosisUpper = maybeMakeRatePretty(apr.osmosis.upper); - const osmosisLower = maybeMakeRatePretty(apr.osmosis.lower); - let boostUpper = maybeMakeRatePretty(apr.boost.upper); - let boostLower = maybeMakeRatePretty(apr.boost.lower); + let total = maybeMakeRatePretty(apr.total_apr); + const swapFee = maybeMakeRatePretty(apr.swap_fees); + const superfluid = maybeMakeRatePretty(apr.superfluid); + const osmosis = maybeMakeRatePretty(apr.osmosis); + let boost = maybeMakeRatePretty(apr.boost); // Temporarily exclude pools in this array from showing boost incentives given an issue on chain if ( ExcludedExternalBoostPools.includes(apr.pool_id) && - totalUpper && - totalLower && - boostUpper && - boostLower + total && + boost ) { - totalUpper = new RatePretty( - totalUpper.toDec().sub(totalUpper.toDec()) - ); - totalLower = new RatePretty( - totalLower.toDec().sub(totalLower.toDec()) - ); - boostUpper = undefined; - boostLower = undefined; + total = new RatePretty(total.toDec().sub(boost.toDec())); + boost = undefined; } // add list of incentives that are defined const incentiveTypes: PoolIncentiveType[] = []; - if (superfluidUpper && superfluidLower) - incentiveTypes.push("superfluid"); - if (osmosisUpper && osmosisLower) incentiveTypes.push("osmosis"); - if (boostUpper && osmosisLower) incentiveTypes.push("boost"); - if ( - !superfluidUpper && - !superfluidLower && - !osmosisUpper && - !osmosisLower && - !boostUpper && - !boostLower - ) - incentiveTypes.push("none"); + if (superfluid) incentiveTypes.push("superfluid"); + if (osmosis) incentiveTypes.push("osmosis"); + if (boost) incentiveTypes.push("boost"); + if (!superfluid && !osmosis && !boost) incentiveTypes.push("none"); const hasBreakdownData = - totalUpper || - totalLower || - swapFeeUpper || - swapFeeLower || - superfluidUpper || - superfluidLower || - osmosisUpper || - osmosisLower || - boostUpper || - boostLower; + total || swapFee || superfluid || osmosis || boost; map.set(apr.pool_id, { aprBreakdown: hasBreakdownData ? { - total: { - upper: totalUpper, - lower: totalLower, - }, - swapFee: { - upper: swapFeeUpper, - lower: swapFeeLower, - }, - superfluid: { - upper: superfluidUpper, - lower: superfluidLower, - }, - osmosis: { - upper: osmosisUpper, - lower: osmosisLower, - }, - boost: { - upper: boostUpper, - lower: boostLower, - }, + total, + swapFee, + superfluid, + osmosis, + boost, } : undefined, incentiveTypes, @@ -219,8 +171,8 @@ export function getConcentratedRangePoolApr({ } function maybeMakeRatePretty(value: number): RatePretty | undefined { - // numia will return 0 or null if the APR is not applicable, so return undefined to indicate that - if (value === 0 || value === null) { + // numia will return 0 if the APR is not applicable, so return undefined to indicate that + if (value === 0) { return undefined; } diff --git a/packages/server/src/queries/data-services/pool-aprs.ts b/packages/server/src/queries/data-services/pool-aprs.ts index ad85e498cf..0dc19a6357 100644 --- a/packages/server/src/queries/data-services/pool-aprs.ts +++ b/packages/server/src/queries/data-services/pool-aprs.ts @@ -2,13 +2,13 @@ import { apiClient } from "@osmosis-labs/utils"; import { NUMIA_BASE_URL } from "../../env"; -type PoolApr = { +type PoolApr = { pool_id: string; - swap_fees: T; - superfluid: T; - osmosis: T; - boost: T; - total_apr: T; + swap_fees: number; + superfluid: number; + osmosis: number; + boost: number; + total_apr: number; }; /** Queries numia for a breakdown of APRs per pool. */ @@ -16,14 +16,3 @@ export function queryPoolAprs(): Promise { const url = new URL("/pools_apr", NUMIA_BASE_URL); return apiClient(url.toString()); } - -export type PoolDataRange = { - lower: T; - upper: T; -}; - -/** Queries numia for a breakdown of APRs per pool with range. */ -export function queryPoolAprsRange(): Promise[]> { - const url = new URL("/pools_apr_range", NUMIA_BASE_URL); - return apiClient(url.toString()); -} diff --git a/packages/trpc/src/pools.ts b/packages/trpc/src/pools.ts index 26ca186e90..9baa873fbd 100644 --- a/packages/trpc/src/pools.ts +++ b/packages/trpc/src/pools.ts @@ -32,7 +32,7 @@ const marketIncentivePoolsSortKeys = [ "feesSpent24hUsd", "volume7dUsd", "volume24hUsd", - "aprBreakdown.total.upper", + "aprBreakdown.total", ] as const; export type MarketIncentivePoolSortKey = (typeof marketIncentivePoolsSortKeys)[number]; diff --git a/packages/web/components/cards/apr-breakdown.tsx b/packages/web/components/cards/apr-breakdown.tsx index 83dfcc7d00..780aa60bef 100644 --- a/packages/web/components/cards/apr-breakdown.tsx +++ b/packages/web/components/cards/apr-breakdown.tsx @@ -1,15 +1,57 @@ import { RatePretty } from "@keplr-wallet/unit"; -import type { PoolDataRange, PoolIncentives } from "@osmosis-labs/server"; +import type { PoolIncentives } from "@osmosis-labs/server"; import classNames from "classnames"; +import { observer } from "mobx-react-lite"; import { FunctionComponent } from "react"; +import { EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS } from "~/config"; import { useTranslation } from "~/hooks"; +import { useStore } from "~/stores"; import { theme } from "~/tailwind.config"; import { Icon } from "../assets"; import { AprDisclaimerTooltip } from "../tooltip/apr-disclaimer"; import { CustomClasses } from "../types"; +/** + * Pools that are excluded from showing external boost incentives APRs. + */ +const ExcludedExternalBoostPools: string[] = + (EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS ?? []) as string[]; + +/** @deprecated uses Mobx query stores, do not use */ +export const AprBreakdownLegacy: FunctionComponent< + { poolId: string; showDisclaimerTooltip?: boolean } & CustomClasses +> = observer(({ poolId, className, showDisclaimerTooltip }) => { + const { queriesExternalStore } = useStore(); + const poolAprs = queriesExternalStore.queryPoolAprs.getForPool(poolId); + + let totalApr = poolAprs?.totalApr; + let boostApr = poolAprs?.boost; + + if ( + poolAprs?.poolId && + ExcludedExternalBoostPools.includes(poolAprs.poolId) && + totalApr && + boostApr + ) { + totalApr = new RatePretty(totalApr.toDec().sub(boostApr.toDec())); + boostApr = undefined; + } + + return ( + + ); +}); + export const AprBreakdown: FunctionComponent< PoolIncentives["aprBreakdown"] & CustomClasses & { showDisclaimerTooltip?: boolean } @@ -25,57 +67,41 @@ export const AprBreakdown: FunctionComponent< const { t } = useTranslation(); return ( -
+
- {swapFee?.upper && swapFee?.lower && ( + {swapFee && ( )} - {osmosis?.upper && osmosis?.lower && ( + {osmosis && (

OSMO {t("pools.aprBreakdown.boost")}

- {osmosis.upper.maxDecimals(1).toString() === - osmosis.lower.maxDecimals(1).toString() ? ( -

{osmosis.upper.maxDecimals(1).toString()}

- ) : ( -

- {osmosis.lower.maxDecimals(1).toString()} -{" "} - {osmosis.upper.maxDecimals(1).toString()} -

- )} +

{osmosis.maxDecimals(1).toString()}

)} - {superfluid?.upper && superfluid?.lower && ( + {superfluid && ( )} - {boost?.upper && boost?.lower && ( + {boost && (

{t("pools.aprBreakdown.externalBoost")}

- {boost.upper.maxDecimals(1).toString() === - boost.lower.maxDecimals(1).toString() ? ( -

{boost.upper.maxDecimals(1).toString()}

- ) : ( -

- {boost.lower.maxDecimals(1).toString()} -{" "} - {boost.upper.maxDecimals(1).toString()} -

- )} +

{boost.maxDecimals(1).toString()}

)}
- {total?.upper && total?.lower && ( + {total && (
{t("pools.aprBreakdown.total")}

)} - {total.upper.maxDecimals(1).toString() === - total.lower.maxDecimals(1).toString() ? ( -

{total.upper.maxDecimals(1).toString()}

- ) : ( -

- {total.lower.maxDecimals(1).toString()} -{" "} - {total.upper.maxDecimals(1).toString()} -

- )} +

{total.maxDecimals(1).toString()}

)}
@@ -109,20 +127,10 @@ export const AprBreakdown: FunctionComponent< const BreakdownRow: FunctionComponent<{ label: string; - value: PoolDataRange; + value: RatePretty; }> = ({ label, value }) => (

{label}

- {value.lower?.maxDecimals(1).toString() === - value.upper?.maxDecimals(1).toString() ? ( -

- {value.upper?.maxDecimals(1).toString()} -

- ) : ( -

- {value.lower?.maxDecimals(2).toString()} -{" "} - {value.upper?.maxDecimals(2).toString()} -

- )} +

{value.maxDecimals(1).toString()}

); diff --git a/packages/web/components/complex/my-pools-card-grid.tsx b/packages/web/components/complex/my-pools-card-grid.tsx index ba2e89e8e2..a8051c9f9a 100644 --- a/packages/web/components/complex/my-pools-card-grid.tsx +++ b/packages/web/components/complex/my-pools-card-grid.tsx @@ -84,10 +84,7 @@ export const MyPoolsCardsGrid = observer(() => { ({ id, type, - apr = { - lower: new RatePretty(0), - upper: new RatePretty(0), - }, + apr = new RatePretty(0), poolLiquidity, userValue, reserveCoins, @@ -99,9 +96,9 @@ export const MyPoolsCardsGrid = observer(() => { { label: t("pools.APR"), value: isMobile ? ( - apr.upper?.maxDecimals(0).toString() ?? "" + apr.maxDecimals(0).toString() ) : ( -
{apr.upper?.maxDecimals(2).toString()}
+
{apr.maxDecimals(2).toString()}
), }, { diff --git a/packages/web/components/complex/pools-table.tsx b/packages/web/components/complex/pools-table.tsx index a2f597b2a1..04be19b4ba 100644 --- a/packages/web/components/complex/pools-table.tsx +++ b/packages/web/components/complex/pools-table.tsx @@ -51,7 +51,7 @@ export const marketIncentivePoolsSortKeys = [ "feesSpent24hUsd", "volume7dUsd", "volume24hUsd", - "aprBreakdown.total.upper", + "aprBreakdown.total", ] as const; export type MarketIncentivePoolsSortKey = @@ -276,7 +276,7 @@ export const PoolsTable = (props: PropsWithChildren) => { header: () => ( - {aprBreakdown.boost?.upper || aprBreakdown.osmosis?.upper ? ( + {aprBreakdown.boost || aprBreakdown.osmosis ? (
) : ( )} - {aprBreakdown?.total?.lower && - aprBreakdown?.total?.upper?.maxDecimals(1).toString() === - aprBreakdown?.total?.lower.maxDecimals(1).toString() ? ( -

{aprBreakdown?.total?.upper?.maxDecimals(1).toString()}

- ) : ( -

- {aprBreakdown?.total?.lower?.maxDecimals(1).toString()} -{" "} - {aprBreakdown?.total?.upper?.maxDecimals(1).toString()} -

- )} + {aprBreakdown.total?.maxDecimals(0).toString() ?? ""}

)) ?? diff --git a/packages/web/components/pool-detail/concentrated.tsx b/packages/web/components/pool-detail/concentrated.tsx index 578eb5234a..270b484bb9 100644 --- a/packages/web/components/pool-detail/concentrated.tsx +++ b/packages/web/components/pool-detail/concentrated.tsx @@ -31,7 +31,7 @@ import { formatPretty, getPriceExtendedFormatOptions } from "~/utils/formatter"; import { api } from "~/utils/trpc"; import { removeQueryParam } from "~/utils/url"; -import { AprBreakdown } from "../cards/apr-breakdown"; +import { AprBreakdownLegacy } from "../cards/apr-breakdown"; import { SkeletonLoader } from "../loaders/skeleton-loader"; const ConcentratedLiquidityDepthChart = dynamic( @@ -462,16 +462,6 @@ const UserAssetsAndExternalIncentives: FunctionComponent<{ poolId: string }> = const hasIncentives = concentratedPoolDetail.incentiveGauges.length > 0; - const { data: incentives, isLoading: isLoadingIncentives } = - api.edge.pools.getPoolIncentives.useQuery( - { - poolId, - }, - { - enabled: featureFlags.aprBreakdown, - } - ); - return (
@@ -513,13 +503,11 @@ const UserAssetsAndExternalIncentives: FunctionComponent<{ poolId: string }> =
{featureFlags.aprBreakdown && ( - - - + )} {hasIncentives && ( diff --git a/packages/web/components/pool-detail/share.tsx b/packages/web/components/pool-detail/share.tsx index ca883d20bf..1d1c966231 100644 --- a/packages/web/components/pool-detail/share.tsx +++ b/packages/web/components/pool-detail/share.tsx @@ -593,8 +593,8 @@ export const SharePool: FunctionComponent<{ pool: Pool }> = observer( {isPoolIncentivesLoading ? ( ) : ( - poolIncentives?.aprBreakdown?.swapFee?.upper && ( -
{`${poolIncentives.aprBreakdown.swapFee.upper + poolIncentives?.aprBreakdown?.swapFee && ( +
{`${poolIncentives.aprBreakdown.swapFee .maxDecimals(2) .toString()} ${t("pool.APR")}`}
) From 21bf5d0bfdb4aed96d0700efe4af522972c67588 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 17 Jul 2024 11:14:10 -0400 Subject: [PATCH 09/10] (Deposit/Withdraw) Recommend Alloyed asset (#3535) * fix quotable provider picking * skip: include counterparties of same variant * capture squid error * use zod for squid errors * i * return providers only for from asset * cleanup * catch parse errors * fix tests --- packages/bridge/src/axelar/index.ts | 2 +- packages/bridge/src/ibc/index.ts | 2 +- packages/bridge/src/skip/index.ts | 20 +++++-- packages/bridge/src/squid/error.ts | 36 +++++++++++++ packages/bridge/src/squid/index.ts | 23 +++++++- .../bridge/amount-and-review-screen.tsx | 54 ++++++++++++++++--- .../web/components/bridge/amount-screen.tsx | 37 +++++-------- 7 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 packages/bridge/src/squid/error.ts diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index 6a7f2f8eb3..f5a775d657 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -345,7 +345,7 @@ export class AxelarBridgeProvider implements BridgeProvider { return foundVariants.assets; } catch (e) { // Avoid returning options if there's an unexpected error, such as the provider being down - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV !== "production") { console.error( AxelarBridgeProvider.ID, "failed to get supported assets:", diff --git a/packages/bridge/src/ibc/index.ts b/packages/bridge/src/ibc/index.ts index fc0a63f21c..8bae5562c7 100644 --- a/packages/bridge/src/ibc/index.ts +++ b/packages/bridge/src/ibc/index.ts @@ -139,7 +139,7 @@ export class IbcBridgeProvider implements BridgeProvider { ]; } catch (e) { // Avoid returning options if there's an unexpected error, such as the provider being down - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV !== "production") { console.error( IbcBridgeProvider.ID, "failed to get supported assets:", diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index 42a0382203..4a7e5910dd 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -264,11 +264,25 @@ export class SkipBridgeProvider implements BridgeProvider { a.coinMinimalDenom.toLowerCase() === asset.address.toLowerCase() ); - for (const counterparty of assetListAsset?.counterparty ?? []) { + const counterparties = assetListAsset?.counterparty ?? []; + // since skip supports cosmos swap, we can include other asset list + // counterparties of the same variant + if (assetListAsset) { + const variantAssets = this.ctx.assetLists.flatMap(({ assets }) => + assets.filter( + (asset) => asset.variantGroupKey === assetListAsset.variantGroupKey + ) + ); + counterparties.push( + ...variantAssets.flatMap((asset) => asset.counterparty) + ); + } + + for (const counterparty of counterparties) { // check if supported by skip if (!("chainId" in counterparty)) continue; if ( - !assets[counterparty.chainId].assets.some( + !assets[counterparty.chainId]?.assets.some( (a) => a.denom.toLowerCase() === counterparty.sourceDenom.toLowerCase() ) @@ -364,7 +378,7 @@ export class SkipBridgeProvider implements BridgeProvider { return foundVariants.assets; } catch (e) { // Avoid returning options if there's an unexpected error, such as the provider being down - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV !== "production") { console.error( SkipBridgeProvider.ID, "failed to get supported assets:", diff --git a/packages/bridge/src/squid/error.ts b/packages/bridge/src/squid/error.ts new file mode 100644 index 0000000000..cdecb53c0a --- /dev/null +++ b/packages/bridge/src/squid/error.ts @@ -0,0 +1,36 @@ +import { ApiClientError } from "@osmosis-labs/utils"; +import { z } from "zod"; + +const SquidErrors = z.object({ + errors: z.array( + z.object({ + path: z.string().optional(), + errorType: z.string(), + message: z.string(), + }) + ), +}); + +type SquidErrors = z.infer; + +/** + * Squid returns error data in the form of an errors object containing an array of errors. + * This function returns the list of those errors, and sets the Error.message to a concatenation of those errors. + * @returns list of error messages + */ +export function getSquidErrors(error: ApiClientError): SquidErrors { + try { + const e = error as ApiClientError; + const squidError = SquidErrors.parse(e.data); + const msgs = squidError.errors.map( + ({ message }, i) => `${i + 1}) ${message}` + ); + e.message = msgs.join(", "); + return squidError; + } catch (e) { + if (e instanceof z.ZodError) { + throw new Error("Squid error validation failed:" + e.errors.join(", ")); + } + throw new Error("Squid errors: An unexpected error occurred"); + } +} diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 4627e63e15..9e05a29a06 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -45,6 +45,7 @@ import { } from "../interface"; import { cosmosMsgOpts, cosmwasmMsgOpts } from "../msg"; import { BridgeAssetMap } from "../utils"; +import { getSquidErrors } from "./error"; const IbcTransferType = "/ibc.applications.transfer.v1.MsgTransfer"; const WasmTransferType = "/cosmwasm.wasm.v1.MsgExecuteContract"; @@ -119,6 +120,26 @@ export class SquidBridgeProvider implements BridgeProvider { headers: { "x-integrator-id": this.integratorId, }, + }).catch((e) => { + if (e instanceof ApiClientError) { + const errMsgs = getSquidErrors(e); + + if ( + errMsgs.errors.some(({ message }) => + message.includes( + "The input amount is not high enough to cover the bridge fee" + ) + ) + ) { + throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, + errorType: "InsufficientAmountError", + message: e.message, + }); + } + } + + throw e; }); const { @@ -349,7 +370,7 @@ export class SquidBridgeProvider implements BridgeProvider { return foundVariants.assets; } catch (e) { // Avoid returning options if there's an unexpected error, such as the provider being down - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV !== "production") { console.error( SquidBridgeProvider.ID, "failed to get supported assets:", diff --git a/packages/web/components/bridge/amount-and-review-screen.tsx b/packages/web/components/bridge/amount-and-review-screen.tsx index 587ab5a8ae..95262f4297 100644 --- a/packages/web/components/bridge/amount-and-review-screen.tsx +++ b/packages/web/components/bridge/amount-and-review-screen.tsx @@ -1,4 +1,5 @@ import { CoinPretty } from "@keplr-wallet/unit"; +import type { Bridge } from "@osmosis-labs/bridge"; import { isNil, noop } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { useMemo, useState } from "react"; @@ -14,7 +15,10 @@ import { AmountScreen } from "./amount-screen"; import { ImmersiveBridgeScreen } from "./immersive-bridge"; import { ReviewScreen } from "./review-screen"; import { QuotableBridge, useBridgeQuotes } from "./use-bridge-quotes"; -import { SupportedAsset } from "./use-bridges-supported-assets"; +import { + SupportedAsset, + useBridgesSupportedAssets, +} from "./use-bridges-supported-assets"; export type SupportedAssetWithAmount = SupportedAsset & { amount: CoinPretty }; @@ -78,17 +82,51 @@ export const AmountAndReviewScreen = observer( ? evmConnector?.icon : toChainCosmosAccount?.walletInfo.logo; + const { data: assetsInOsmosis } = + api.edge.assets.getCanonicalAssetWithVariants.useQuery( + { + findMinDenomOrSymbol: selectedAssetDenom ?? "", + }, + { + enabled: !isNil(selectedAssetDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); + + const supportedAssets = useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); + const { supportedAssetsByChainId: counterpartySupportedAssetsByChainId } = + supportedAssets; + /** Filter for bridges that currently support quoting. */ const quoteBridges = useMemo(() => { - const assetSupportedBridges = - (direction === "deposit" - ? fromAsset?.supportedVariants[toAsset?.address ?? ""] - : toAsset?.supportedVariants[fromAsset?.address ?? ""]) ?? []; + const assetSupportedBridges = new Set(); + + if (direction === "deposit" && fromAsset) { + Object.values(fromAsset.supportedVariants) + .flat() + .forEach((provider) => assetSupportedBridges.add(provider)); + } else if (direction === "withdraw" && fromAsset && toAsset) { + // withdraw + counterpartySupportedAssetsByChainId[toAsset.chainId].forEach( + (asset) => { + asset.supportedVariants[fromAsset.address]?.forEach((provider) => { + assetSupportedBridges.add(provider); + }); + } + ); + } - return assetSupportedBridges.filter( + return Array.from(assetSupportedBridges).filter( (bridge) => bridge !== "Nomic" && bridge !== "Wormhole" ) as QuotableBridge[]; - }, [direction, fromAsset, toAsset]); + }, [direction, fromAsset, toAsset, counterpartySupportedAssetsByChainId]); const quote = useBridgeQuotes({ toAddress, @@ -141,6 +179,8 @@ export const AmountAndReviewScreen = observer( ; + fromChain: BridgeChainWithDisplayInfo | undefined; setFromChain: (chain: BridgeChainWithDisplayInfo) => void; toChain: BridgeChainWithDisplayInfo | undefined; @@ -100,6 +103,12 @@ export const AmountScreen = observer( direction, selectedDenom, + assetsInOsmosis, + bridgesSupportedAssets: { + supportedAssetsByChainId: counterpartySupportedAssetsByChainId, + supportedChains, + }, + fromChain, setFromChain, toChain, @@ -199,18 +208,6 @@ export const AmountScreen = observer( ? evmAddress : toCosmosCounterpartyAccount?.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, }); @@ -229,17 +226,6 @@ export const AmountScreen = observer( canonicalAsset ); - const { - supportedAssetsByChainId: counterpartySupportedAssetsByChainId, - supportedChains, - } = useBridgesSupportedAssets({ - assets: assetsInOsmosis, - chain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - }); - const firstSupportedEvmChain = useMemo( () => supportedChains.find( @@ -886,7 +872,8 @@ export const AmountScreen = observer( {(direction === "deposit" ? !isNil(fromAsset) && Object.keys(fromAsset.supportedVariants).length > 1 - : !isNil(toAsset) && + : // direction === "withdraw" + !isNil(toAsset) && counterpartySupportedAssetsByChainId[toAsset.chainId]?.length > 1) && ( From 09f2224a47cc9b3c8e157a7178d7abeab7e850c0 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 17 Jul 2024 12:16:55 -0400 Subject: [PATCH 10/10] only set max if no quotes are loading (#3543) --- .../web/components/bridge/amount-screen.tsx | 5 +++++ .../components/bridge/crypto-fiat-input.tsx | 10 ++++++---- .../components/bridge/use-bridge-quotes.ts | 20 ++++++++++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/web/components/bridge/amount-screen.tsx b/packages/web/components/bridge/amount-screen.tsx index 6ca8ec9e86..06456c7681 100644 --- a/packages/web/components/bridge/amount-screen.tsx +++ b/packages/web/components/bridge/amount-screen.tsx @@ -688,6 +688,11 @@ export const AmountScreen = observer( isInsufficientBal={Boolean(isInsufficientBal)} isInsufficientFee={Boolean(isInsufficientFee)} transferGasCost={selectedQuote?.gasCost} + /** Wait for all quotes to resolve before modifying input amount. + * This helps reduce thrash while the best quote is being determined. + * Only once we get the best quote, we can modify the input amount + * to account for gas then restart the quote search process. */ + canSetMax={!quote.isLoadingAnyBridgeQuote} setFiatAmount={setFiatAmount} setCryptoAmount={setCryptoAmount} setInputUnit={setInputUnit} diff --git a/packages/web/components/bridge/crypto-fiat-input.tsx b/packages/web/components/bridge/crypto-fiat-input.tsx index b4084684ce..f6373a7452 100644 --- a/packages/web/components/bridge/crypto-fiat-input.tsx +++ b/packages/web/components/bridge/crypto-fiat-input.tsx @@ -37,6 +37,7 @@ export const CryptoFiatInput: FunctionComponent<{ isInsufficientFee: boolean; fromChain: BridgeChainWithDisplayInfo; transferGasCost: CoinPretty | undefined; + canSetMax?: boolean; setFiatAmount: (amount: string) => void; setCryptoAmount: (amount: string) => void; setInputUnit: (unit: "fiat" | "crypto") => void; @@ -50,6 +51,7 @@ export const CryptoFiatInput: FunctionComponent<{ isInsufficientBal, isInsufficientFee, transferGasCost, + canSetMax = true, setFiatAmount: setFiatAmountProp, setCryptoAmount: setCryptoAmountProp, setInputUnit, @@ -152,7 +154,7 @@ export const CryptoFiatInput: FunctionComponent<{ // Subtract gas cost and adjust input when selecting max amount useEffect(() => { - if (isMax && transferGasCost) { + if (isMax && transferGasCost && canSetMax) { let maxTransferAmount = new Dec(0); const gasFeeMatchesInputDenom = @@ -175,14 +177,14 @@ export const CryptoFiatInput: FunctionComponent<{ onInput("crypto")(trimPlaceholderZeros(maxTransferAmount.toString())); } } - }, [isMax, transferGasCost, asset.amount, inputCoin, onInput]); + }, [isMax, canSetMax, transferGasCost, asset.amount, inputCoin, onInput]); // Apply max amount if asset changes useEffect(() => { - if (isMax) { + if (isMax && canSetMax) { onInput("crypto")(trimPlaceholderZeros(asset.amount.toDec().toString())); } - }, [asset, isMax, onInput]); + }, [asset, isMax, canSetMax, onInput]); const fiatCurrentValue = `${assetPrice.symbol}${fiatInputRaw}`; const fiatInputFontSize = calcTextSizeClass( diff --git a/packages/web/components/bridge/use-bridge-quotes.ts b/packages/web/components/bridge/use-bridge-quotes.ts index 460dfd053a..f1d28b3054 100644 --- a/packages/web/components/bridge/use-bridge-quotes.ts +++ b/packages/web/components/bridge/use-bridge-quotes.ts @@ -287,8 +287,8 @@ export const useBridgeQuotes = ({ const numSucceeded = successfulQuotes.length; const isOneSuccessful = Boolean(numSucceeded); - const amountOfErrors = erroredQuotes.length; - const isOneErrored = Boolean(amountOfErrors); + const isAllSuccessful = numSucceeded === bridges.length; + const isOneErrored = Boolean(erroredQuotes.length); // if none have returned a resulting quote, find some error const someError = useMemo( @@ -653,17 +653,25 @@ export const useBridgeQuotes = ({ (!isOneSuccessful || quoteResults.every((quoteResult) => quoteResult.isLoading)) && quoteResults.some((quoteResult) => quoteResult.fetchStatus !== "idle"); + const isLoadingAnyBridgeQuote = quoteResults.some( + (quoteResult) => quoteResult.isLoading && quoteResult.fetchStatus !== "idle" + ); const isLoadingBridgeTransaction = bridgeTransaction.isLoading && bridgeTransaction.fetchStatus !== "idle"; - const isWithdrawReady = isWithdraw && !isTxPending; - const isWalletConnected = + const isWithdrawReady = + isWithdraw && !isTxPending && !isLoadingBridgeTransaction; + const isFromWalletConnected = fromChain?.chainType === "evm" ? isEvmWalletConnected : fromChain?.chainType === "cosmos" ? accountStore.getWallet(fromChain.chainId)?.isWalletConnected ?? false : false; const isDepositReady = - isDeposit && isWalletConnected && !isLoadingBridgeQuote && !isTxPending; + isDeposit && + isFromWalletConnected && + !isLoadingBridgeQuote && + !isTxPending && + !isLoadingBridgeTransaction; const userCanAdvance = (isDepositReady || isWithdrawReady) && !isInsufficientFee && @@ -711,6 +719,7 @@ export const useBridgeQuotes = ({ warnUserOfPriceImpact, successfulQuotes, + isAllQuotesSuccessful: isAllSuccessful, selectedBridgeProvider, setSelectedBridgeProvider: onChangeBridgeProvider, @@ -718,6 +727,7 @@ export const useBridgeQuotes = ({ selectedQuoteUpdatedAt: selectedQuoteQuery?.dataUpdatedAt, refetchInterval, isLoadingBridgeQuote, + isLoadingAnyBridgeQuote, isLoadingBridgeTransaction, isRefetchingQuote: selectedQuoteQuery?.isRefetching ?? false, };