Skip to content

Commit

Permalink
(Deposit/Withdraw) Recommend Alloyed asset (#3535)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jonator authored Jul 17, 2024
1 parent 9d3acb3 commit 21bf5d0
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 38 deletions.
2 changes: 1 addition & 1 deletion packages/bridge/src/axelar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/src/ibc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
20 changes: 17 additions & 3 deletions packages/bridge/src/skip/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Expand Down Expand Up @@ -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:",
Expand Down
36 changes: 36 additions & 0 deletions packages/bridge/src/squid/error.ts
Original file line number Diff line number Diff line change
@@ -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<typeof SquidErrors>;

/**
* 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<SquidErrors>;
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");
}
}
23 changes: 22 additions & 1 deletion packages/bridge/src/squid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:",
Expand Down
54 changes: 47 additions & 7 deletions packages/web/components/bridge/amount-and-review-screen.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 };

Expand Down Expand Up @@ -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<Bridge>();

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,
Expand Down Expand Up @@ -141,6 +179,8 @@ export const AmountAndReviewScreen = observer(
<AmountScreen
direction={direction}
selectedDenom={selectedAssetDenom!}
assetsInOsmosis={assetsInOsmosis}
bridgesSupportedAssets={supportedAssets}
fromChain={fromChain}
setFromChain={setFromChain}
toChain={toChain}
Expand Down
37 changes: 12 additions & 25 deletions packages/web/components/bridge/amount-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
MenuItem,
MenuItems,
} from "@headlessui/react";
import { BridgeTransactionDirection } from "@osmosis-labs/types";
import { BridgeTransactionDirection, MinimalAsset } from "@osmosis-labs/types";
import { getShortAddress, isNil, noop } from "@osmosis-labs/utils";
import classNames from "classnames";
import { observer } from "mobx-react-lite";
Expand Down Expand Up @@ -71,6 +71,9 @@ interface AmountScreenProps {
direction: "deposit" | "withdraw";
selectedDenom: string;

assetsInOsmosis: MinimalAsset[] | undefined;
bridgesSupportedAssets: ReturnType<typeof useBridgesSupportedAssets>;

fromChain: BridgeChainWithDisplayInfo | undefined;
setFromChain: (chain: BridgeChainWithDisplayInfo) => void;
toChain: BridgeChainWithDisplayInfo | undefined;
Expand Down Expand Up @@ -100,6 +103,12 @@ export const AmountScreen = observer(
direction,
selectedDenom,

assetsInOsmosis,
bridgesSupportedAssets: {
supportedAssetsByChainId: counterpartySupportedAssetsByChainId,
supportedChains,
},

fromChain,
setFromChain,
toChain,
Expand Down Expand Up @@ -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,
});
Expand All @@ -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(
Expand Down Expand Up @@ -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) && (
<Menu>
Expand Down

0 comments on commit 21bf5d0

Please sign in to comment.