From 0e2e6f31733ddcab4028762ebabbba6afe893f75 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Tue, 25 Jun 2024 19:20:15 -0500 Subject: [PATCH 01/15] add provider to root (#3404) --- .../bridge/src/ibc/__tests__/ibc-transfer-status.spec.ts | 8 ++++---- packages/bridge/src/ibc/index.ts | 2 ++ packages/bridge/src/ibc/transfer-status.ts | 2 +- packages/bridge/src/index.ts | 1 + packages/web/stores/root.ts | 2 ++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/bridge/src/ibc/__tests__/ibc-transfer-status.spec.ts b/packages/bridge/src/ibc/__tests__/ibc-transfer-status.spec.ts index bf751e5508..deb698167b 100644 --- a/packages/bridge/src/ibc/__tests__/ibc-transfer-status.spec.ts +++ b/packages/bridge/src/ibc/__tests__/ibc-transfer-status.spec.ts @@ -4,7 +4,7 @@ import { TxTracer } from "@osmosis-labs/tx"; import { MockAssetLists } from "../../__tests__/mock-asset-lists"; import { MockChains } from "../../__tests__/mock-chains"; import { TransferStatusReceiver } from "../../interface"; -import { IBCTransferStatusProvider } from "../transfer-status"; +import { IbcTransferStatusProvider } from "../transfer-status"; const makeRpcStatusResponse = ( timeoutHeight: string, @@ -67,8 +67,8 @@ jest.mock("@osmosis-labs/tx", () => ({ })), })); -describe("IBCTransferStatusProvider", () => { - let provider: IBCTransferStatusProvider; +describe("IbcTransferStatusProvider", () => { + let provider: IbcTransferStatusProvider; let mockReceiver: jest.Mocked; let consoleSpy: jest.SpyInstance; @@ -76,7 +76,7 @@ describe("IBCTransferStatusProvider", () => { mockReceiver = { receiveNewTxStatus: jest.fn(), }; - provider = new IBCTransferStatusProvider(MockChains, MockAssetLists); + provider = new IbcTransferStatusProvider(MockChains, MockAssetLists); provider.statusReceiverDelegate = mockReceiver; // silences console errors and serves as a spy to test for calls consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); diff --git a/packages/bridge/src/ibc/index.ts b/packages/bridge/src/ibc/index.ts index bcfbfc799a..f699b31da7 100644 --- a/packages/bridge/src/ibc/index.ts +++ b/packages/bridge/src/ibc/index.ts @@ -280,3 +280,5 @@ export class IbcBridgeProvider implements BridgeProvider { return { urlProviderName: "TFM", url }; } } + +export * from "./transfer-status"; diff --git a/packages/bridge/src/ibc/transfer-status.ts b/packages/bridge/src/ibc/transfer-status.ts index 86ecffd5f9..85f86a6c7f 100644 --- a/packages/bridge/src/ibc/transfer-status.ts +++ b/packages/bridge/src/ibc/transfer-status.ts @@ -13,7 +13,7 @@ import { IbcBridgeProvider } from "."; export type IbcTransferStatus = "pending" | "complete" | "timeout" | "refunded"; -export class IBCTransferStatusProvider implements TransferStatusProvider { +export class IbcTransferStatusProvider implements TransferStatusProvider { readonly keyPrefix = IbcBridgeProvider.ID; readonly sourceDisplayName = "IBC Transfer"; public statusReceiverDelegate?: TransferStatusReceiver; diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index 44e9c34053..c9116a9098 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -2,6 +2,7 @@ export * from "./axelar"; export * from "./bridge-providers"; export * from "./chain"; export * from "./errors"; +export * from "./ibc"; export * from "./interface"; export * from "./skip"; export * from "./squid"; diff --git a/packages/web/stores/root.ts b/packages/web/stores/root.ts index 36d5bacb5d..826bf331db 100644 --- a/packages/web/stores/root.ts +++ b/packages/web/stores/root.ts @@ -1,5 +1,6 @@ import { AxelarTransferStatusProvider, + IbcTransferStatusProvider, SkipTransferStatusProvider, SquidTransferStatusProvider, } from "@osmosis-labs/bridge"; @@ -256,6 +257,7 @@ export class RootStore { new AxelarTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), new SquidTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), new SkipTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), + new IbcTransferStatusProvider(ChainList, AssetLists), ] ); From e0e628db295efe4bb84dad81421aaaf6af0b68a1 Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:46:54 +0200 Subject: [PATCH 02/15] Remove Stage frontend monitoring tests (#3412) --- .github/workflows/monitoring-e2e-tests.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/monitoring-e2e-tests.yml b/.github/workflows/monitoring-e2e-tests.yml index bad72b13dc..6d2185836e 100644 --- a/.github/workflows/monitoring-e2e-tests.yml +++ b/.github/workflows/monitoring-e2e-tests.yml @@ -16,12 +16,8 @@ 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: ${{ matrix.env }} - needs: setup-matrix + name: production runs-on: macos-latest - strategy: - fail-fast: false - matrix: ${{fromJson(needs.setup-matrix.outputs.matrix)}} environment: name: prod_swap_test steps: @@ -43,9 +39,9 @@ jobs: - name: Install Playwright run: | yarn --cwd packages/web install --frozen-lockfile && npx playwright install --with-deps chromium - - name: Run Swap tests on ${{ matrix.env }} + - name: Run Swap tests on production env: - BASE_URL: ${{ matrix.base-url }} + BASE_URL: "https://app.osmosis.zone" PRIVATE_KEY_S: ${{ secrets.PRIVATE_KEY_S }} run: | cd packages/web @@ -55,7 +51,7 @@ jobs: id: e2e-test-results uses: actions/upload-artifact@v4 with: - name: ${{ matrix.env }}-e2e-test-results + name: production-e2e-test-results path: packages/web/playwright-report - name: Send Slack alert if test fails id: slack @@ -77,7 +73,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*Environment:* ${{ matrix.env }}\n*App URL:* ${{ matrix.base-url }}" + "text": "*Environment:* production\n*App URL:* https://app.osmosis.zone" } }, { From e40bcb03c9fcc78487d1666fb3a98c746b209e16 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 26 Jun 2024 10:37:10 -0500 Subject: [PATCH 03/15] Bridge Providers: remove `sourceDenom` (#3389) * Auto stash before merge of "jon/fe-573-bridge-providers-remove-sourcedenom" and "stage" * update tests, remove sourceDenom * fix build * WIP remove sourceDenom * fix tests * use v2 API * remove log --- packages/bridge/README.md | 19 +- .../__tests__/axelar-bridge-provider.spec.ts | 210 ++----- .../mock-axelar-assets-and-chains.ts | 27 + packages/bridge/src/axelar/index.ts | 394 ++++++------- packages/bridge/src/axelar/queries.ts | 3 +- packages/bridge/src/axelar/tokens.ts | 2 + packages/bridge/src/errors.ts | 7 +- .../ibc/__tests__/ibc-bridge-provider.spec.ts | 28 +- packages/bridge/src/ibc/index.ts | 43 +- packages/bridge/src/interface.ts | 30 +- packages/bridge/src/skip/__tests__/mocks.ts | 508 +++++++++++++++++ .../__tests__/skip-bridge-provider.spec.ts | 454 ++++++--------- packages/bridge/src/skip/index.ts | 153 +++--- packages/bridge/src/skip/queries.ts | 4 +- packages/bridge/src/squid/__tests__/mocks.ts | 516 ++++++++++++++++++ .../__tests__/squid-bridge-provider.spec.ts | 283 +++++----- packages/bridge/src/squid/index.ts | 63 +-- .../src/queries/complex/assets/price/index.ts | 16 +- .../components/bridge/more-bridge-options.tsx | 2 - packages/web/components/complex/transfer.tsx | 21 +- packages/web/hooks/use-feature-flags.ts | 1 + .../ethereum/hooks/use-erc20-balance.ts | 3 +- .../trust-walletconnect/client.ts | 9 +- packages/web/modals/bridge-transfer-v2.tsx | 14 +- .../web/server/api/routers/bridge-transfer.ts | 40 +- 25 files changed, 1832 insertions(+), 1018 deletions(-) create mode 100644 packages/bridge/src/skip/__tests__/mocks.ts diff --git a/packages/bridge/README.md b/packages/bridge/README.md index d0ea834455..4811b0891c 100644 --- a/packages/bridge/README.md +++ b/packages/bridge/README.md @@ -65,11 +65,11 @@ class MyBridgeProvider implements BridgeProvider { providerName = "MyBridge"; logoUrl = "url_to_logo"; - async getQuote(params: GetBridgeQuoteParams): Promise { + getQuote(params: GetBridgeQuoteParams): Promise { // Implement logic to get a quote for a cross-chain transfer } - async getTransferStatus( + getTransferStatus( params: GetTransferStatusParams ): Promise { // Implement logic to get the status of a transfer @@ -81,6 +81,12 @@ class MyBridgeProvider implements BridgeProvider { // Implement logic to get transaction data } + getSupportedAssets( + params: GetBridgeSupportedAssetsParams + ): Promise<(BridgeChain & BridgeAsset)[]> { + // Implement logic to get supported assets + } + getDepositAddress?( params: GetDepositAddressParams ): Promise { @@ -121,6 +127,9 @@ export interface BridgeProvider { getTransactionData( params: GetBridgeQuoteParams ): Promise; + getSupportedAssets( + params: GetBridgeSupportedAssetsParams + ): Promise<(BridgeChain & BridgeAsset)[]>; getDepositAddress?( params: GetDepositAddressParams ): Promise; @@ -158,14 +167,13 @@ export interface BridgeTransferStatus { ### BridgeAsset -The BridgeAsset interface represents an asset that can be transferred across a bridge. It includes the denomination of the asset, the address of the asset, the number of decimal places for the asset, and the source minimum denom (e.g. uatom). +The BridgeAsset interface represents an asset that can be transferred across a bridge. It includes the denomination of the asset, the address of the asset on a given chain, the number of decimal places for the asset. ```tsx export interface BridgeAsset { denom: string; address: string; decimals: number; - sourceDenom: string; } ``` @@ -177,7 +185,8 @@ The BridgeCoin type represents an asset with an amount, likely returned within a export type BridgeCoin = { denom: string; decimals: number; - sourceDenom: string; + /** The address of the asset, represented as an IBC denom, origin denom, or EVM contract address. */ + address: string; /** Amount without decimals. */ amount: string; }; diff --git a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts index 237f75d685..75344f4eb7 100644 --- a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts +++ b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts @@ -97,12 +97,21 @@ describe("AxelarBridgeProvider", () => { const depositAddress = await provider.getDepositAddress({ fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, - toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, + toChain: { + chainId: "osmosis-1", + chainName: "Osmosis", + chainType: "cosmos", + }, fromAsset: { denom: "ETH", - address: "0x0", + address: NativeEVMTokenConstantAddress, + decimals: 18, + }, + toAsset: { + denom: "ETH.axl", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, - sourceDenom: "eth", }, toAddress: "0x456", }); @@ -129,13 +138,15 @@ describe("AxelarBridgeProvider", () => { denom: "ETH", address: "0x0", decimals: 18, - sourceDenom: "eth", + }, + toAsset: { + denom: "ETH", + address: "0x0", + decimals: 18, }, toAddress: "0x456", }) - ).rejects.toThrow( - "Unsupported chain: Chain ID 989898989898 is not supported." - ); + ).rejects.toThrow("Chain not found: 989898989898"); }); it("should estimate gas cost for EVM transactions", async () => { @@ -143,16 +154,14 @@ describe("AxelarBridgeProvider", () => { fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, fromAsset: { - denom: "ETH", - address: "0x0", + denom: "WETH", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { - denom: "AVAX", - address: "0x0", + denom: "axlETH", + address: "0x42A62eb3Fd2a05eD499117F128de8a3192B49EBB", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x1234567890abcdef1234567890abcdef12345678", @@ -164,7 +173,7 @@ describe("AxelarBridgeProvider", () => { // Should be a string representation of a number expect(gasCost?.amount).toMatch(/^\d+(\.\d+)?$/); expect(gasCost?.denom).toBe("ETH"); - expect(gasCost?.sourceDenom).toBe("ETH"); + expect(gasCost?.address).toBe("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"); expect(gasCost?.decimals).toBe(18); }); @@ -195,13 +204,11 @@ describe("AxelarBridgeProvider", () => { address: "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, - sourceDenom: "uusdc", }, toAsset: { denom: "USDc", address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6, - sourceDenom: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", }, fromAmount: "1", fromAddress: "cosmos1ABC123", @@ -213,11 +220,11 @@ describe("AxelarBridgeProvider", () => { // Should be a string representation of a number expect(gasCost!.amount).toBe("1000"); expect(gasCost!.denom).toBe("OSMO"); - expect(gasCost!.sourceDenom).toBe("uosmo"); + expect(gasCost!.address).toBe("uosmo"); expect(gasCost!.decimals).toBe(6); }); - it("should create an EVM transaction", async () => { + it("should create an EVM transaction - ERC20 transfer", async () => { const mockDepositClient: Partial = { getDepositAddress: jest .fn() @@ -232,16 +239,14 @@ describe("AxelarBridgeProvider", () => { fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, fromAsset: { - denom: "ETH", - address: "0x0000000000000000000000000000000000000000", + denom: "WETH", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { - denom: "AVAX", - address: "0x0000000000000000000000000000000000000000", + denom: "axlETH", + address: "0x42A62eb3Fd2a05eD499117F128de8a3192B49EBB", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x1234567890abcdef1234567890abcdef12345678", @@ -252,11 +257,11 @@ describe("AxelarBridgeProvider", () => { expect(transaction).toEqual({ data: "0xa9059cbb0000000000000000000000001234567890abcdef1234567890abcdef123456780000000000000000000000000000000000000000000000000000000000000001", type: "evm", - to: "0x0000000000000000000000000000000000000000", + to: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // ERC20 contract address of fromAsset (WETH) }); }); - it("should create an EVM transaction with native token", async () => { + it("should create an EVM transaction - native send", async () => { const mockDepositClient: Partial = { getDepositAddress: jest .fn() @@ -274,13 +279,11 @@ describe("AxelarBridgeProvider", () => { denom: "ETH", address: NativeEVMTokenConstantAddress, decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0x0000000000000000000000000000000000000000", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x1234567890abcdef1234567890abcdef12345678", @@ -295,41 +298,6 @@ describe("AxelarBridgeProvider", () => { }); }); - it("should throw an error when creating an EVM transaction with a non-native token", async () => { - const mockDepositClient: Partial = { - getDepositAddress: jest - .fn() - .mockResolvedValue("0x1234567890abcdef1234567890abcdef12345678"), - }; - - jest - .spyOn(provider, "getAssetTransferClient") - .mockResolvedValue(mockDepositClient as unknown as AxelarAssetTransfer); - - await expect( - provider.createEvmTransaction({ - fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, - toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, - fromAsset: { - denom: "ETH", - address: NativeEVMTokenConstantAddress, - decimals: 6, - sourceDenom: "weth", - }, - toAsset: { - denom: "ETH", - address: NativeEVMTokenConstantAddress, - decimals: 6, - sourceDenom: "eth", - }, - fromAmount: "1", - fromAddress: "0x1234567890abcdef1234567890abcdef12345678", - toAddress: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef", - simulated: false, - }) - ).rejects.toThrow("eth is not a native token on Axelar"); - }); - it("should create a Cosmos transaction", async () => { const mockDepositClient: Partial = { getDepositAddress: jest @@ -353,13 +321,11 @@ describe("AxelarBridgeProvider", () => { address: "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, - sourceDenom: "uusdc", }, toAsset: { denom: "USDC", address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6, - sourceDenom: "uusdc", }, fromAmount: "1000000", fromAddress: "cosmos1...", @@ -417,13 +383,11 @@ describe("AxelarBridgeProvider", () => { denom: "ETH", address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", @@ -434,10 +398,15 @@ describe("AxelarBridgeProvider", () => { expect(quote).toBeDefined(); expect(quote).toEqual({ estimatedTime: 900, - input: { amount: "1", sourceDenom: "eth", decimals: 18, denom: "ETH" }, + input: { + amount: "1", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + denom: "ETH", + }, expectedOutput: { amount: "0.990000000000000000", - sourceDenom: "avax", + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", decimals: 18, denom: "AVAX", priceImpact: "0", @@ -447,13 +416,14 @@ describe("AxelarBridgeProvider", () => { transferFee: { amount: "0.01", denom: "ETH", - sourceDenom: "eth", + chainId: 1, + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, }, estimatedGasFee: { amount: "420000000000000", denom: "ETH", - sourceDenom: "ETH", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, }, }); @@ -488,13 +458,11 @@ describe("AxelarBridgeProvider", () => { denom: "ETH", address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1.1", fromAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", @@ -516,16 +484,14 @@ describe("AxelarBridgeProvider", () => { fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, fromAsset: { - denom: "ETH", - address: "0x0", + denom: "WETH", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { - denom: "AVAX", - address: "0x0", + denom: "axlETH", + address: "0x42A62eb3Fd2a05eD499117F128de8a3192B49EBB", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x123", @@ -535,47 +501,6 @@ describe("AxelarBridgeProvider", () => { ).rejects.toThrow("Query client error"); }); - it("should throw an error when withdrawing native asset without using 'autoUnwrapIntoNative'", async () => { - const mockDepositClient: Partial = { - getDepositAddress: jest - .fn() - .mockResolvedValue("0x1234567890abcdef1234567890abcdef12345678"), - }; - - jest - .spyOn(provider, "getAssetTransferClient") - .mockResolvedValue(mockDepositClient as unknown as AxelarAssetTransfer); - - await expect( - provider.createCosmosTransaction({ - fromChain: { - chainId: "osmosis-1", - chainName: "Osmosis", - chainType: "cosmos", - }, - toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, - fromAsset: { - denom: "ETH", - address: NativeEVMTokenConstantAddress, - decimals: 6, - sourceDenom: "eth", - }, - toAsset: { - denom: "ETH", - address: NativeEVMTokenConstantAddress, - decimals: 6, - sourceDenom: "eth", - }, - fromAmount: "1000000", - fromAddress: "cosmos1...", - toAddress: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef", - simulated: false, - }) - ).rejects.toThrow( - "When withdrawing native ETH from Axelar, use the 'autoUnwrapIntoNative' option and not the native minimal denom" - ); - }); - describe("getSupportedAssets", () => { it("gets source axelar assets - Ethereum USDC", async () => { const sourceVariants = await provider.getSupportedAssets({ @@ -588,7 +513,6 @@ describe("AxelarBridgeProvider", () => { address: "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -600,7 +524,6 @@ describe("AxelarBridgeProvider", () => { chainType: "evm", decimals: 6, denom: "USDC", - sourceDenom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", }, ]); }); @@ -616,7 +539,6 @@ describe("AxelarBridgeProvider", () => { address: "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 6, - sourceDenom: "weth-wei", }, }); @@ -628,17 +550,15 @@ describe("AxelarBridgeProvider", () => { chainType: "evm", decimals: 18, denom: "WETH", - sourceDenom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, { // this is the denom accepted by Axelar APIs - address: "eth", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", chainId: 1, chainName: "Ethereum", chainType: "evm", decimals: 18, denom: "ETH", - sourceDenom: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", }, ]); }); @@ -672,7 +592,6 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { toChain: { chainId: "osmosis-1", chainType: "cosmos" }, fromAsset: { denom: "ETH", - sourceDenom: "weth-wei", decimals: 18, address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, @@ -681,24 +600,22 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, denom: "ETH", - sourceDenom: "weth-wei", }, toAddress: "destination-address", }); - expect(result?.urlProviderName).toBe("Skip"); + expect(result?.urlProviderName).toBe("Satellite Money"); expect(result?.url.toString()).toBe( - "https://satellite.money/?source=Ethereum&destination=osmosis&asset_denom=weth-wei&destination_address=destination-address" + "https://satellite.money/?source=ethereum&destination=osmosis&asset_denom=weth-wei&destination_address=destination-address" ); }); - it("should return the correct URL for Eth <> axlEth", async () => { + it("should return the correct URL for ETH <> axlEth", async () => { const result = await provider.getExternalUrl({ fromChain: { chainId: 1, chainType: "evm" }, toChain: { chainId: "osmosis-1", chainType: "cosmos" }, fromAsset: { denom: "ETH", - sourceDenom: "weth-wei", decimals: 18, address: NativeEVMTokenConstantAddress, }, @@ -707,14 +624,13 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, denom: "ETH", - sourceDenom: "weth-wei", }, toAddress: "destination-address", }); - expect(result?.urlProviderName).toBe("Skip"); + expect(result?.urlProviderName).toBe("Satellite Money"); expect(result?.url.toString()).toBe( - "https://satellite.money/?source=Ethereum&destination=osmosis&asset_denom=eth&destination_address=destination-address" + "https://satellite.money/?source=ethereum&destination=osmosis&asset_denom=eth&destination_address=destination-address" ); }); @@ -724,7 +640,6 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { toChain: { chainId: "osmosis-1", chainType: "cosmos" }, fromAsset: { denom: "USDC", - sourceDenom: "uusdc", decimals: 6, address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", }, @@ -733,14 +648,13 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, denom: "USDC", - sourceDenom: "uusdc", }, toAddress: "destination-address", }); - expect(result?.urlProviderName).toBe("Skip"); + expect(result?.urlProviderName).toBe("Satellite Money"); expect(result?.url.toString()).toBe( - "https://satellite.money/?source=Ethereum&destination=osmosis&asset_denom=uusdc&destination_address=destination-address" + "https://satellite.money/?source=ethereum&destination=osmosis&asset_denom=uusdc&destination_address=destination-address" ); }); @@ -750,7 +664,6 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { toChain: { chainId: "osmosis-1", chainType: "cosmos" }, fromAsset: { denom: "USDC", - sourceDenom: "uusdc", decimals: 6, address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", }, @@ -759,14 +672,13 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, denom: "USDC", - sourceDenom: "uusdc", }, toAddress: "destination-address", }); - expect(result?.urlProviderName).toBe("Skip"); + expect(result?.urlProviderName).toBe("Satellite Money"); expect(result?.url.toString()).toBe( - "https://satellite.money/?source=Avalanche&destination=osmosis&asset_denom=uusdc&destination_address=destination-address" + "https://satellite.money/?source=avalanche&destination=osmosis&asset_denom=avalanche-uusdc&destination_address=destination-address" ); }); @@ -778,13 +690,11 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { fromAsset: { address: "address1", denom: "denom1", - sourceDenom: "sourceDenom1", decimals: 18, }, toAsset: { address: "address2", denom: "denom2", - sourceDenom: "sourceDenom2", decimals: 18, }, toAddress: "destination-address", @@ -800,13 +710,11 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { fromAsset: { address: "address1", denom: "denom1", - sourceDenom: "sourceDenom1", decimals: 18, }, toAsset: { address: "address2", denom: "denom2", - sourceDenom: "sourceDenom2", decimals: 18, }, toAddress: "destination-address", @@ -820,19 +728,17 @@ describe("AxelarBridgeProvider.getExternalUrl", () => { fromChain: { chainId: 1, chainType: "evm" }, toChain: { chainId: "osmosis-1", chainType: "cosmos" }, fromAsset: { - address: "address1", + address: "nonexistent", denom: "denom1", - sourceDenom: "sourceDenom1", decimals: 18, }, toAsset: { - address: "nonexistent", + address: "address2", denom: "denom2", - sourceDenom: "sourceDenom2", decimals: 18, }, toAddress: "destination-address", }) - ).rejects.toThrow("Asset not found: nonexistent"); + ).rejects.toThrow("Axelar source asset not found: nonexistent"); }); }); diff --git a/packages/bridge/src/axelar/__tests__/mock-axelar-assets-and-chains.ts b/packages/bridge/src/axelar/__tests__/mock-axelar-assets-and-chains.ts index a38a73c344..0c933ea5fd 100644 --- a/packages/bridge/src/axelar/__tests__/mock-axelar-assets-and-chains.ts +++ b/packages/bridge/src/axelar/__tests__/mock-axelar-assets-and-chains.ts @@ -686,6 +686,33 @@ export const MockAxelarAssets = [ coingecko_id: "axelar", id: "uaxl", }, + { + denom: "avalanche-uusdc", + native_chain: "avalanche", + name: "Avalanche USDC", + symbol: "AvalancheUSDC", + decimals: 6, + image: "/logos/assets/usdc.svg", + addresses: { + avalanche: { + symbol: "AvalancheUSDC", + address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + }, + axelarnet: { symbol: "AvalancheUSDC", ibc_denom: "avalanche-uusdc" }, + osmosis: { + symbol: "AvalancheUSDC", + ibc_denom: + "ibc/F17C9CA112815613C5B6771047A093054F837C3020CBA59DFFD9D780A8B2984C", + }, + sommelier: { + symbol: "AvalancheUSDC", + ibc_denom: + "ibc/4AF49824065A2EB5F05E5AD5823178E595577D26500592615E3E8746EBDA5652", + }, + }, + coingecko_id: "usd-coin", + id: "avalanche-uusdc", + }, ]; export const MockAxelarChains = [ diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index ced70a0bd9..dadb7e0a02 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -7,11 +7,7 @@ import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import type { IbcTransferMethod } from "@osmosis-labs/types"; -import { - getAssetFromAssetList, - getKeyByValue, - isNil, -} from "@osmosis-labs/utils"; +import { getAssetFromAssetList } from "@osmosis-labs/utils"; import { cachified } from "cachified"; import { Address, @@ -33,22 +29,17 @@ import { BridgeProvider, BridgeProviderContext, BridgeQuote, - BridgeSupportedAssetsParams, BridgeTransactionRequest, CosmosBridgeTransactionRequest, EvmBridgeTransactionRequest, GetBridgeExternalUrlParams, GetBridgeQuoteParams, + GetBridgeSupportedAssetsParams, GetDepositAddressParams, } from "../interface"; import { cosmosMsgOpts } from "../msg"; import { BridgeAssetMap } from "../utils"; import { getAxelarAssets, getAxelarChains } from "./queries"; -import { AxelarSourceChainTokenConfigs } from "./tokens"; -import { - AxelarChainIds_SourceChainMap, - CosmosChainIds_AxelarChainIds, -} from "./types"; export class AxelarBridgeProvider implements BridgeProvider { static readonly ID = "Axelar"; @@ -97,34 +88,31 @@ export class AxelarBridgeProvider implements BridgeProvider { toChain, slippage, }), + ttl: process.env.NODE_ENV === "test" ? -1 : 20 * 1000, // 20 seconds getFreshValue: async (): Promise => { try { - const amount = new CoinPretty( - { - coinDecimals: fromAsset.decimals, - coinDenom: fromAsset.denom, - coinMinimalDenom: fromAsset.sourceDenom ?? fromAsset.denom, - }, - fromAmount - ).toCoin().amount; - - const fromChainAxelarId = this.getAxelarChainId(fromChain); - const toChainAxelarId = this.getAxelarChainId(toChain); + const [fromChainAxelarId, toChainAxelarId, fromAssetAxelarId] = + await Promise.all([ + this.getAxelarChainId(fromChain), + this.getAxelarChainId(toChain), + this.getAxelarAssetId(fromChain, fromAsset), + ]); if (!fromChainAxelarId || !toChainAxelarId) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "Axelar Bridge doesn't support this quote", }); } const queryClient = await this.getQueryClient(); - const [transferFeeRes, gasCost] = await Promise.all([ + const [transferFeeRes, estimatedGasFee] = await Promise.all([ queryClient.getTransferFee( fromChainAxelarId, toChainAxelarId, - fromAsset.sourceDenom, - amount as any + fromAssetAxelarId, + fromAmount as any ), this.estimateGasCost(params), ]); @@ -133,9 +121,9 @@ export class AxelarBridgeProvider implements BridgeProvider { try { /** Returns value in denom */ transferLimitAmount = await queryClient.getTransferLimit({ - denom: fromAsset.sourceDenom, - fromChainId: fromChainAxelarId.toLowerCase(), - toChainId: toChainAxelarId.toLowerCase(), + denom: fromAssetAxelarId, + fromChainId: fromChainAxelarId, + toChainId: toChainAxelarId, }); } catch (e) { console.warn("Failed to get transfer limit. reason: ", e); @@ -143,6 +131,7 @@ export class AxelarBridgeProvider implements BridgeProvider { if (!transferFeeRes.fee) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "Axelar Bridge doesn't support this quote", }); @@ -153,12 +142,13 @@ export class AxelarBridgeProvider implements BridgeProvider { new Dec(fromAmount).gte(new Dec(transferLimitAmount)) ) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: `Amount exceeds transfer limit of ${new CoinPretty( { coinDecimals: fromAsset.decimals, coinDenom: fromAsset.denom, - coinMinimalDenom: fromAsset.sourceDenom, + coinMinimalDenom: fromAsset.address, }, new Dec(transferLimitAmount) ) @@ -167,12 +157,6 @@ export class AxelarBridgeProvider implements BridgeProvider { }); } - const transferFeeAsset = getAssetFromAssetList({ - /** Denom from Axelar's `getTransferFee` is the min denom */ - sourceDenom: transferFeeRes.fee.denom, - assetLists: this.ctx.assetLists, - }); - const expectedOutputAmount = new Dec(fromAmount).sub( new Dec(transferFeeRes.fee.amount) ); @@ -180,41 +164,28 @@ export class AxelarBridgeProvider implements BridgeProvider { return { estimatedTime: this.getWaitTime(fromChainAxelarId), input: { + ...fromAsset, amount: fromAmount, - sourceDenom: fromAsset.sourceDenom, - decimals: fromAsset.decimals, - denom: fromAsset.denom, }, expectedOutput: { + ...toAsset, amount: expectedOutputAmount.toString(), - sourceDenom: toAsset.sourceDenom, - decimals: toAsset.decimals, - denom: toAsset.denom, priceImpact: "0", }, fromChain, toChain, transferFee: { + ...fromAsset, amount: transferFeeRes.fee.amount, - denom: - transferFeeAsset?.symbol ?? - fromAsset.denom ?? - transferFeeRes.fee.denom, - sourceDenom: fromAsset.sourceDenom, - decimals: fromAsset.decimals, + chainId: fromChain.chainId, + denom: fromAsset.denom ?? transferFeeRes.fee.denom, }, - ...(gasCost && { - estimatedGasFee: { - amount: gasCost.amount, - denom: gasCost.denom, - sourceDenom: gasCost.sourceDenom, - decimals: gasCost.decimals, - }, - }), + estimatedGasFee, }; } catch (e) { if (typeof e === "string" && e.includes("not found")) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: e, }); @@ -223,14 +194,13 @@ export class AxelarBridgeProvider implements BridgeProvider { throw e; } }, - ttl: 20 * 1000, // 20 seconds, }); } async getSupportedAssets({ chain, asset, - }: BridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { + }: GetBridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { try { // get origin axelar asset info from given toAsset const [axelarAssets, axelarChains] = await Promise.all([ @@ -244,10 +214,12 @@ export class AxelarBridgeProvider implements BridgeProvider { const axelarSourceAsset = axelarAssets.find(({ addresses }) => Object.keys(addresses).some( (address) => - addresses[address]?.ibc_denom?.toLowerCase() === - asset.address?.toLowerCase() || - addresses[address]?.address?.toLowerCase() === - asset.address?.toLowerCase() + (addresses[address]?.ibc_denom && + addresses[address]!.ibc_denom!.toLowerCase() === + asset.address.toLowerCase()) || + (addresses[address]?.address && + addresses[address]!.address!.toLowerCase() === + asset.address.toLowerCase()) ) ); @@ -256,44 +228,45 @@ export class AxelarBridgeProvider implements BridgeProvider { "Axelar source asset not found given asset address: " + asset.address ); - // make sure to chain and to asset align (validation) - const axelarChainId = this.getAxelarChainId(chain); - if (!axelarChainId) - throw new Error("Axelar chain not ID found: " + chain.chainId); - const axelarAssetAddress = axelarSourceAsset.addresses[axelarChainId]; + // make sure given chain and asset align (validation) + // validate chain + const axelarChainId = await this.getAxelarChainId(chain); + const axelarChainAssetAddress = + axelarSourceAsset.addresses[axelarChainId]; if ( - !axelarAssetAddress || + !axelarChainAssetAddress || + // validate asset (asset.address.toLowerCase() !== - axelarAssetAddress.ibc_denom?.toLowerCase() && + axelarChainAssetAddress.ibc_denom?.toLowerCase() && asset.address.toLowerCase() !== - axelarAssetAddress.address?.toLowerCase()) + axelarChainAssetAddress.address?.toLowerCase()) ) throw new Error( - "Axelar asset address not found, axelarChainId: " + axelarChainId + "Chain and asset combo not recognized by Axelar asset & chain list, axelarChainId: " + + axelarChainId ); const foundVariants = new BridgeAssetMap(); // return just origin asset and the unwrapped version for now, but // can return other axl-versions later if wanted - const nativeChainAsset = + const addressAsset = axelarSourceAsset.addresses[axelarSourceAsset.native_chain]; - if (!nativeChainAsset) + if (!addressAsset) throw new Error( "Native chain asset not found, asset native_chain: " + axelarSourceAsset.native_chain ); - const sourceAssetId = - nativeChainAsset?.address ?? nativeChainAsset?.ibc_denom; - if (!sourceAssetId) + const assetAddress = addressAsset?.address ?? addressAsset?.ibc_denom; + if (!assetAddress) throw new Error( "Source asset ID not found, native chain asset ID: " + - nativeChainAsset?.address ?? nativeChainAsset?.ibc_denom + addressAsset?.address ?? addressAsset?.ibc_denom ); const axelarChain = axelarChains.find( - (chain) => chain.maintainer_id === axelarSourceAsset.native_chain + (chain) => chain.id.toLowerCase() === axelarSourceAsset.native_chain ); if (!axelarChain) @@ -319,26 +292,20 @@ export class AxelarBridgeProvider implements BridgeProvider { axelarSourceAsset.denom, { ...chainInfo, - chainName: axelarChain.chain_name, - denom: nativeChainAsset.symbol, - address: sourceAssetId, + chainName: axelarChain.name, + denom: addressAsset.symbol, + address: assetAddress, decimals: axelarSourceAsset.decimals, - sourceDenom: sourceAssetId, } ); - // there are auto-un/wrapped versions + // there are auto-un/wrapped versions for EVM if (axelarSourceAsset.denoms) { // assume it's the chain native asset const unwrappedDenom = axelarSourceAsset.denoms[1]; if (!unwrappedDenom) return foundVariants.assets; - const axelarChain = axelarChains.find( - (chain) => chain.maintainer_id === axelarSourceAsset.native_chain - ); - - if (!axelarChain) return foundVariants.assets; // only handle unwrapping with evm chains due to ERC20 standard & EVM account model if (axelarChain.chain_type !== "evm") return foundVariants.assets; @@ -349,11 +316,10 @@ export class AxelarBridgeProvider implements BridgeProvider { // axelar chain list IDs are canonical chainId: axelarChain.chain_id as number, chainType: axelarChain.chain_type, - chainName: axelarChain.chain_name, + chainName: axelarChain.name, denom: axelarChain.native_token.symbol, - address: unwrappedDenom, + address: NativeEVMTokenConstantAddress, decimals: axelarChain.native_token.decimals, - sourceDenom: NativeEVMTokenConstantAddress, } ); } @@ -396,7 +362,9 @@ export class AxelarBridgeProvider implements BridgeProvider { await fromProvider.estimateGas({ account: params.fromAddress as Address, to: transactionData.to, - value: BigInt(transactionData.value ?? ""), + value: transactionData.value + ? BigInt(transactionData.value) + : undefined, data: transactionData.data, }) ); @@ -406,7 +374,7 @@ export class AxelarBridgeProvider implements BridgeProvider { const gasCost = new Dec(gasAmountUsed).mul(new Dec(gasPrice)); return { amount: gasCost.truncate().toString(), - sourceDenom: evmChain.nativeCurrency.symbol, + address: NativeEVMTokenConstantAddress, decimals: evmChain.nativeCurrency.decimals, denom: evmChain.nativeCurrency.symbol, }; @@ -436,7 +404,7 @@ export class AxelarBridgeProvider implements BridgeProvider { amount: gasFee.amount, denom: gasAsset?.symbol ?? gasFee.denom, decimals: gasAsset?.decimals ?? 0, - sourceDenom: gasAsset?.coinMinimalDenom ?? gasFee.denom, + address: gasAsset?.coinMinimalDenom ?? gasFee.denom, }; } } @@ -458,6 +426,7 @@ export class AxelarBridgeProvider implements BridgeProvider { fromAsset, fromChain, toChain, + toAsset, toAddress, fromAmount, simulated, @@ -465,37 +434,17 @@ export class AxelarBridgeProvider implements BridgeProvider { }: GetBridgeQuoteParams & { simulated?: boolean; }): Promise { - const isNativeToken = this.isNativeAsset(fromAsset); - - if ( - isNativeToken && - // is wrapped token - !Object.values(AxelarSourceChainTokenConfigs(this.ctx.env)).some( - (chain) => { - return Object.values(chain).some( - ({ nativeWrapEquivalent }) => - nativeWrapEquivalent && - nativeWrapEquivalent.tokenMinDenom === fromAsset.sourceDenom - ); - } - ) - ) { - throw new BridgeQuoteError({ - errorType: "CreateEVMTxError", - message: `${fromAsset.sourceDenom} is not a native token on Axelar`, - }); - } - const { depositAddress } = simulated ? { depositAddress: fromAddress } : await this.getDepositAddress({ fromChain, toChain, fromAsset, + toAsset, toAddress, }); - if (isNativeToken) { + if (this.isNativeEvmToken(fromAsset)) { return { type: "evm", to: depositAddress as Address, @@ -508,7 +457,7 @@ export class AxelarBridgeProvider implements BridgeProvider { data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", - args: [depositAddress as `0x${string}`, BigInt(fromAmount)], + args: [depositAddress as Address, BigInt(fromAmount)], }), }; } @@ -518,35 +467,14 @@ export class AxelarBridgeProvider implements BridgeProvider { fromChain, toChain, fromAsset, + toAsset, fromAddress, toAddress, - toAsset, fromAmount, simulated, }: GetBridgeQuoteParams & { simulated?: boolean; }): Promise { - const isNativeToken = this.isNativeAsset(toAsset); - - if ( - isNativeToken && - // is native token - Object.values(AxelarSourceChainTokenConfigs(this.ctx.env)).some( - (chain) => { - return Object.values(chain).some( - ({ nativeWrapEquivalent }) => - nativeWrapEquivalent && - nativeWrapEquivalent.tokenMinDenom === toAsset.sourceDenom - ); - } - ) - ) { - throw new BridgeQuoteError({ - errorType: "CreateEVMTxError", - message: `When withdrawing native ${fromAsset.denom} from Axelar, use the 'autoUnwrapIntoNative' option and not the native minimal denom`, - }); - } - try { const { depositAddress } = simulated ? { depositAddress: fromAddress } @@ -554,9 +482,8 @@ export class AxelarBridgeProvider implements BridgeProvider { fromChain, toChain, fromAsset, + toAsset, toAddress, - autoUnwrapIntoNative: - fromChain.chainType === "cosmos" && isNativeToken, }); const timeoutHeight = await this.ctx.getTimeoutHeight({ @@ -565,11 +492,13 @@ export class AxelarBridgeProvider implements BridgeProvider { const ibcAsset = getAssetFromAssetList({ assetLists: this.ctx.assetLists, + // Explicitly check against coinMinimalDenom coinMinimalDenom: fromAsset.address, }); if (!ibcAsset) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "CreateCosmosTxError", message: "Could not find IBC asset info", }); @@ -581,6 +510,7 @@ export class AxelarBridgeProvider implements BridgeProvider { if (!ibcTransferMethod) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "CreateCosmosTxError", message: "Could not find IBC asset transfer info", }); @@ -612,6 +542,7 @@ export class AxelarBridgeProvider implements BridgeProvider { if (error instanceof Error) { throw new BridgeQuoteError({ + bridgeId: AxelarBridgeProvider.ID, errorType: "CreateCosmosTxError", message: error.message, }); @@ -625,36 +556,34 @@ export class AxelarBridgeProvider implements BridgeProvider { fromChain, toChain, fromAsset, + toAsset, toAddress, - autoUnwrapIntoNative, }: GetDepositAddressParams): Promise { - const fromChainAxelarId = this.getAxelarChainId(fromChain); - const toChainAxelarId = this.getAxelarChainId(toChain); - - if (!fromChainAxelarId || !toChainAxelarId) { - throw new Error( - `Unsupported chain: Chain ID ${ - !fromChainAxelarId ? fromChain.chainId : toChain.chainId - } is not supported.` - ); - } + const fromChainAxelarId = await this.getAxelarChainId(fromChain); + const toChainAxelarId = await this.getAxelarChainId(toChain); + const autoUnwrapIntoNative = + fromChain.chainType === "cosmos" && this.isNativeEvmToken(toAsset); return cachified({ cache: this.ctx.cache, key: `${ AxelarBridgeProvider.ID }${fromChainAxelarId}_${toChainAxelarId}/${toAddress}/${ - fromAsset.sourceDenom + fromAsset.address }/${Boolean(autoUnwrapIntoNative)}`, + ttl: process.env.NODE_ENV === "test" ? -1 : 30 * 60 * 1000, // 30 minutes getFreshValue: async (): Promise => { - const depositClient = await this.getAssetTransferClient(); + const [depositClient, toAssetAxelarId] = await Promise.all([ + this.getAssetTransferClient(), + this.getAxelarAssetId(fromChain, fromAsset), + ]); return { depositAddress: await depositClient.getDepositAddress({ fromChain: fromChainAxelarId, toChain: toChainAxelarId, destinationAddress: toAddress, - asset: fromAsset.sourceDenom, + asset: toAssetAxelarId, options: autoUnwrapIntoNative ? { shouldUnwrapIntoNative: autoUnwrapIntoNative, @@ -663,39 +592,103 @@ export class AxelarBridgeProvider implements BridgeProvider { }), }; }, - ttl: 30 * 60 * 1000, // 30 minutes }); } - isNativeAsset(asset: BridgeAsset) { + isNativeEvmToken(asset: BridgeAsset) { return asset.address === NativeEVMTokenConstantAddress; } - getWaitTime(sourceChain: string) { - switch (sourceChain) { - case "Ethereum": - case "Polygon": + getWaitTime(axelarChainId: string) { + switch (axelarChainId) { + case "ethereum": + case "polygon": return 900; default: return 180; } } - getAxelarChainId(chain: GetBridgeQuoteParams["fromChain"]) { - if (chain.chainType === "cosmos") { - return CosmosChainIds_AxelarChainIds(this.ctx.env)[chain.chainId]; + /** + * Returns asset ID (denom) considered canonical by Axelar and its APIs. + * @throws if not found in Axelar chain or asset list. + */ + async getAxelarAssetId( + chain: BridgeChain, + asset: BridgeAsset + ): Promise { + const axelarAssets = await getAxelarAssets({ env: this.ctx.env }); + + // Use chain ID and find axelar asset ID from asset's denoms list + // Only applicable to native EVM assets + if (this.isNativeEvmToken(asset)) { + const axelarChains = await getAxelarChains({ env: this.ctx.env }); + + const axelarChain = axelarChains.find( + ({ chain_id }) => chain_id === chain.chainId + ); + if (!axelarChain) { + throw new Error("Chain not found"); + } + + // Use chain ID and find axelar asset ID from denoms list + const axelarNativeEvmAsset = axelarAssets.find( + ({ native_chain, denoms }) => + native_chain === axelarChain.id && + // The second denom matches the native gas token denom as a lowercase symbol + denoms && + denoms[1].toLowerCase() === + axelarChain.native_token.symbol.toLowerCase() + ); + + if (!axelarNativeEvmAsset || !axelarNativeEvmAsset.denoms) { + throw new Error("Axelar gas asset not found"); + } + + return axelarNativeEvmAsset.denoms[1]; + } + + // Match asset address against some Axelar asset's list of addresses + const axelarSourceAsset = axelarAssets.find(({ addresses }) => + Object.keys(addresses).some( + (address) => + addresses[address]?.ibc_denom?.toLowerCase() === + asset.address.toLowerCase() || + addresses[address]?.address?.toLowerCase() === + asset.address.toLowerCase() + ) + ); + + if (!axelarSourceAsset) { + throw new Error("Axelar source asset not found: " + asset.address); } - const ethereumChainName = Object.values(EthereumChainInfo).find( - ({ id: chainId }) => String(chainId) === String(chain.chainId) - )?.chainName; + // Indicates asset is autowrappable, Axelar APIs accept the wrapped denom here + if (axelarSourceAsset.denoms) { + return axelarSourceAsset.denoms[0]; + } + + // Asset is not autowrappable, the denom is the ID accepted by Axelar APIs + return axelarSourceAsset.denom; + } - if (!ethereumChainName) return undefined; + /** + * Returns chain ID considered canonical by Axelar and its APIs. + * @throws if not found in Axelar chain or asset list. + */ + async getAxelarChainId({ chainId }: BridgeChain) { + const axelarChains = await getAxelarChains({ env: this.ctx.env }); - return getKeyByValue( - AxelarChainIds_SourceChainMap(this.ctx.env), - ethereumChainName + // Axelar's chain_id is the canonical chain ID used in most registries + const axelarChain = axelarChains.find( + ({ chain_id }) => chain_id === chainId ); + + if (!axelarChain) { + throw new Error(`Chain not found: ${chainId}`); + } + + return axelarChain.id; } async initClients() { @@ -741,68 +734,27 @@ export class AxelarBridgeProvider implements BridgeProvider { async getExternalUrl({ fromChain, toChain, - toAsset, - toAddress, fromAsset, + toAddress, }: GetBridgeExternalUrlParams): Promise { - const [axelarChains, axelarAssets] = await Promise.all([ - getAxelarChains({ env: this.ctx.env }), - getAxelarAssets({ env: this.ctx.env }), + const [fromChainId, toChainId, toAssetId] = await Promise.all([ + this.getAxelarChainId(fromChain), + this.getAxelarChainId(toChain), + this.getAxelarAssetId(fromChain, fromAsset), ]); - const fromAxelarChain = axelarChains.find( - (chain) => String(chain.chain_id) === String(fromChain.chainId) - ); - - if (!fromAxelarChain) { - throw new Error(`Chain not found: ${fromChain.chainId}`); - } - - const toAxelarChain = axelarChains.find( - (chain) => String(chain.chain_id) === String(toChain.chainId) - ); - - if (!toAxelarChain) { - throw new Error(`Chain not found: ${toChain.chainId}`); - } - - const fromAxelarAsset = axelarAssets.find((axelarAsset) => { - const asset = axelarAsset.addresses[toAxelarChain.chain_name]; - if (isNil(asset)) return false; - - const ibcDenomMatches = - asset.ibc_denom?.toLowerCase() === toAsset.address?.toLowerCase(); - const addressMatches = - asset.address?.toLowerCase() === toAsset.address?.toLowerCase(); - - return ibcDenomMatches || addressMatches; - }); - - if (!fromAxelarAsset) { - throw new Error(`Asset not found: ${toAsset.address}`); - } - const url = new URL( this.ctx.env === "mainnet" ? "https://satellite.money/" : "https://testnet.satellite.money/" ); - url.searchParams.set("source", fromAxelarChain.chain_name); - url.searchParams.set("destination", toAxelarChain.chain_name); - url.searchParams.set( - "asset_denom", - // Check if the asset has multiple denoms (indicating wrapped tokens) - !isNil(fromAxelarAsset.denoms) && fromAxelarAsset.denoms.length > 1 - ? // If the fromAsset address is the native EVM token constant address, use the second denom which is the unwrapped token - fromAsset.address === NativeEVMTokenConstantAddress - ? fromAxelarAsset.denoms[1] - : fromAxelarAsset.denoms[0] - : // If there are no multiple denoms, use the default denom - fromAxelarAsset.denom - ); + url.searchParams.set("source", fromChainId); + url.searchParams.set("destination", toChainId); + // asset_denom denotes the selection of the from asset + url.searchParams.set("asset_denom", toAssetId); url.searchParams.set("destination_address", toAddress); - return { urlProviderName: "Skip", url }; + return { urlProviderName: "Satellite Money", url }; } } diff --git a/packages/bridge/src/axelar/queries.ts b/packages/bridge/src/axelar/queries.ts index 0aad47e28e..fb29413a1b 100644 --- a/packages/bridge/src/axelar/queries.ts +++ b/packages/bridge/src/axelar/queries.ts @@ -97,7 +97,7 @@ interface AxelarChain { id: string; chain_id: number | string; chain_name: string; - maintainer_id: string; + maintainer_id?: string; endpoints: { rpc: string[]; lcd: string[]; @@ -106,6 +106,7 @@ interface AxelarChain { symbol: string; name: string; decimals: number; + denom?: string; }; name: string; short_name: string; diff --git a/packages/bridge/src/axelar/tokens.ts b/packages/bridge/src/axelar/tokens.ts index 099ebbf76c..f2c7b5e43d 100644 --- a/packages/bridge/src/axelar/tokens.ts +++ b/packages/bridge/src/axelar/tokens.ts @@ -2,6 +2,7 @@ import { SourceChain } from "../chain"; import { EthereumChainInfo } from "../ethereum"; import { BridgeEnvironment } from "../interface"; +/** @deprecated Prefer using Axelar chain/asset list API via bridge providers instead */ export type SourceChainTokenConfig = { /** Source Chain identifier. */ id: SourceChain; @@ -40,6 +41,7 @@ export type SourceChainTokenConfig = { /** https://axelarscan.io/assets * Ensure that users bridge sufficient amounts from EthMainnet=>NonEthEvm via Axelar before enabling. + * @deprecated Prefer using Axelar chain/asset list API via bridge providers instead */ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { [asset: string]: { [chain: string]: SourceChainTokenConfig }; diff --git a/packages/bridge/src/errors.ts b/packages/bridge/src/errors.ts index 9e7479dd9a..d733640ad1 100644 --- a/packages/bridge/src/errors.ts +++ b/packages/bridge/src/errors.ts @@ -1,15 +1,18 @@ -/** Includes a `BridgeError` in the message member. */ +/** Includes a `BridgeError` & optionally the bridge ID in the message member. */ export class BridgeQuoteError extends Error { readonly errorType: BridgeError; constructor({ + bridgeId, errorType, message, }: { + bridgeId?: string; errorType: BridgeError; message: string; }) { - super(`${errorType}: ${message}`); + const id = bridgeId ? `(${bridgeId}) ` : ""; + super(`${id}${errorType}: ${message}`); this.errorType = errorType; } } diff --git a/packages/bridge/src/ibc/__tests__/ibc-bridge-provider.spec.ts b/packages/bridge/src/ibc/__tests__/ibc-bridge-provider.spec.ts index 814c38ae51..117ad48c5b 100644 --- a/packages/bridge/src/ibc/__tests__/ibc-bridge-provider.spec.ts +++ b/packages/bridge/src/ibc/__tests__/ibc-bridge-provider.spec.ts @@ -37,14 +37,12 @@ const mockAtomToOsmosis: GetBridgeQuoteParams = { }, fromAsset: { address: "uatom", - sourceDenom: "uatom", denom: "ATOM", decimals: 6, }, toAsset: { address: "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - sourceDenom: "uatom", denom: "ATOM", decimals: 6, }, @@ -65,13 +63,11 @@ const mockAtomFromOsmosis: GetBridgeQuoteParams = { fromAsset: { address: "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", - sourceDenom: "uatom", denom: "ATOM", decimals: 6, }, toAsset: { address: "uatom", - sourceDenom: "uatom", denom: "ATOM", decimals: 6, }, @@ -166,7 +162,7 @@ describe("IbcBridgeProvider", () => { public getIbcSourcePublic(params: GetBridgeQuoteParams): { sourceChannel: string; sourcePort: string; - sourceDenom: string; + address: string; } { return this.getIbcSource(params); } @@ -183,7 +179,7 @@ describe("IbcBridgeProvider", () => { expect(result.sourceChannel).toBe("channel-0"); expect(result.sourcePort).toBe("transfer"); - expect(result.sourceDenom).toBe( + expect(result.address).toBe( "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" ); }); @@ -193,7 +189,7 @@ describe("IbcBridgeProvider", () => { expect(result.sourceChannel).toBe("channel-141"); expect(result.sourcePort).toBe("transfer"); - expect(result.sourceDenom).toBe("uatom"); + expect(result.address).toBe("uatom"); }); it("should throw if asset not found", () => { @@ -201,11 +197,11 @@ describe("IbcBridgeProvider", () => { ...mockAtomToOsmosis, toAsset: { ...mockAtomToOsmosis.toAsset, - sourceDenom: "not-found", + address: "not-found", }, fromAsset: { ...mockAtomToOsmosis.fromAsset, - sourceDenom: "not-found", + address: "not-found", }, }; @@ -219,11 +215,11 @@ describe("IbcBridgeProvider", () => { ...mockAtomToOsmosis, toAsset: { ...mockAtomToOsmosis.toAsset, - sourceDenom: "uosmo", + address: "uosmo", }, fromAsset: { ...mockAtomToOsmosis.fromAsset, - sourceDenom: "uosmo", + address: "uosmo", }, }; @@ -245,7 +241,6 @@ describe("IbcBridgeProvider", () => { address: "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", decimals: 6, - sourceDenom: "uatom", }, }); @@ -256,7 +251,6 @@ describe("IbcBridgeProvider", () => { denom: "ATOM", address: "uatom", decimals: 6, - sourceDenom: "uatom", }, ]); }); @@ -276,13 +270,11 @@ describe("IbcBridgeProvider.getExternalUrl", () => { fromChain: { chainId: 1, chainType: "evm" }, toChain: { chainId: "cosmoshub-4", chainType: "cosmos" }, fromAsset: { - sourceDenom: "weth-wei", address: "weth-wei", decimals: 18, denom: "WETH", }, toAsset: { - sourceDenom: "uatom", address: "uatom", decimals: 6, denom: "ATOM", @@ -297,8 +289,8 @@ describe("IbcBridgeProvider.getExternalUrl", () => { const params = { fromChain: { chainId: "osmosis-1", chainType: "cosmos" }, toChain: { chainId: 1, chainType: "evm" }, - fromAsset: { sourceDenom: "uosmo" }, - toAsset: { sourceDenom: "weth-wei" }, + fromAsset: { address: "uosmo" }, + toAsset: { address: "weth-wei" }, } as Parameters[0]; const url = await provider.getExternalUrl(params); @@ -313,13 +305,11 @@ describe("IbcBridgeProvider.getExternalUrl", () => { fromChain: { chainId: "osmosis-1", chainType: "cosmos" }, toChain: { chainId: "cosmoshub-4", chainType: "cosmos" }, fromAsset: { - sourceDenom: "uosmo", address: "uosmo", decimals: 6, denom: "OSMO", }, toAsset: { - sourceDenom: "uatom", address: "uatom", decimals: 6, denom: "ATOM", diff --git a/packages/bridge/src/ibc/index.ts b/packages/bridge/src/ibc/index.ts index f699b31da7..aa198ef0c8 100644 --- a/packages/bridge/src/ibc/index.ts +++ b/packages/bridge/src/ibc/index.ts @@ -12,10 +12,10 @@ import { BridgeProvider, BridgeProviderContext, BridgeQuote, - BridgeSupportedAssetsParams, CosmosBridgeTransactionRequest, GetBridgeExternalUrlParams, GetBridgeQuoteParams, + GetBridgeSupportedAssetsParams, } from "../interface"; import { cosmosMsgOpts } from "../msg"; @@ -38,6 +38,7 @@ export class IbcBridgeProvider implements BridgeProvider { params.toChain.chainType !== "cosmos" ) { throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "IBC Bridge only supports cosmos chains", }); @@ -73,6 +74,7 @@ export class IbcBridgeProvider implements BridgeProvider { if (new Int(toAmount).lte(new Int(0))) { throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "InsufficientAmountError", message: "Insufficient amount for fees", }); @@ -93,6 +95,7 @@ export class IbcBridgeProvider implements BridgeProvider { // currently subsidized by relayers, but could be paid by user in future by charging the user the gas cost of transferFee: { ...params.fromAsset, + chainId: fromChainId, amount: "0", }, estimatedTime: 6, @@ -100,7 +103,7 @@ export class IbcBridgeProvider implements BridgeProvider { amount: gasFee.amount, denom: gasFee.denom, // should be same as denom since it's on the same chain - sourceDenom: gasFee.denom, + address: gasFee.denom, decimals: gasAsset?.decimals ?? 6, }, transactionRequest: signDoc, @@ -109,15 +112,11 @@ export class IbcBridgeProvider implements BridgeProvider { async getSupportedAssets({ asset, - }: BridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { + }: GetBridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { try { const assetListAsset = this.ctx.assetLists .flatMap((list) => list.assets) - .find( - (a) => - a.coinMinimalDenom === asset.address || - a.sourceDenom === asset.sourceDenom - ); + .find((a) => a.coinMinimalDenom === asset.address); const ibcTransferMethod = assetListAsset?.transferMethods.find( ({ type }) => type === "ibc" @@ -135,7 +134,6 @@ export class IbcBridgeProvider implements BridgeProvider { address: assetListAsset.sourceDenom, denom: assetListAsset.symbol, decimals: assetListAsset.decimals, - sourceDenom: ibcTransferMethod.counterparty.sourceDenom, }, ]; } catch (e) { @@ -155,8 +153,7 @@ export class IbcBridgeProvider implements BridgeProvider { ): Promise { this.validate(params); - const { sourceChannel, sourcePort, sourceDenom } = - this.getIbcSource(params); + const { sourceChannel, sourcePort, address } = this.getIbcSource(params); const timeoutHeight = await this.ctx.getTimeoutHeight({ destinationAddress: params.toAddress, @@ -172,7 +169,7 @@ export class IbcBridgeProvider implements BridgeProvider { timeoutHeight, token: { amount: params.fromAmount, - denom: sourceDenom, + denom: address, }, }); @@ -191,20 +188,21 @@ export class IbcBridgeProvider implements BridgeProvider { protected getIbcSource({ fromAsset, toAsset }: GetBridgeQuoteParams): { sourceChannel: string; sourcePort: string; - sourceDenom: string; + address: string; } { const transferAsset = this.ctx.assetLists .flatMap((list) => list.assets) .find( (asset) => - asset.coinMinimalDenom === toAsset.sourceDenom || - asset.sourceDenom === toAsset.sourceDenom || - asset.coinMinimalDenom === fromAsset.sourceDenom || - asset.sourceDenom === fromAsset.sourceDenom + asset.coinMinimalDenom === toAsset.address || + asset.sourceDenom === toAsset.address || + asset.coinMinimalDenom === fromAsset.address || + asset.sourceDenom === fromAsset.address ); if (!transferAsset) throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "CreateCosmosTxError", message: "IBC asset not found in asset list", }); @@ -215,6 +213,7 @@ export class IbcBridgeProvider implements BridgeProvider { if (!transferMethod) throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "CreateCosmosTxError", message: "IBC transfer method not found", }); @@ -225,7 +224,7 @@ export class IbcBridgeProvider implements BridgeProvider { return { sourceChannel: channelId, sourcePort: port, - sourceDenom, + address: sourceDenom, }; } else { // transfer from source @@ -233,7 +232,7 @@ export class IbcBridgeProvider implements BridgeProvider { return { sourceChannel: channelId, sourcePort: port, - sourceDenom: fromAsset.address, + address: fromAsset.address, }; } } @@ -245,6 +244,7 @@ export class IbcBridgeProvider implements BridgeProvider { params.toAsset.address.startsWith("cw20") ) { throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "IBC Bridge doesn't support cw20 standard", }); @@ -255,6 +255,7 @@ export class IbcBridgeProvider implements BridgeProvider { params.toChain.chainType !== "cosmos" ) { throw new BridgeQuoteError({ + bridgeId: IbcBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "IBC Bridge only supports cosmos chains", }); @@ -273,9 +274,9 @@ export class IbcBridgeProvider implements BridgeProvider { const url = new URL("https://geo.tfm.com/"); url.searchParams.set("chainFrom", fromChain.chainId); - url.searchParams.set("token0", fromAsset.sourceDenom); + url.searchParams.set("token0", fromAsset.address); url.searchParams.set("chainTo", toChain.chainId); - url.searchParams.set("token1", toAsset.sourceDenom); + url.searchParams.set("token1", toAsset.address); return { urlProviderName: "TFM", url }; } diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index 96521c586e..f4b791278e 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -55,7 +55,7 @@ export interface BridgeProvider { * @returns A promise that resolves to an array of assets combined with each assets' chain info. */ getSupportedAssets( - params: BridgeSupportedAssetsParams + params: GetBridgeSupportedAssetsParams ): Promise<(BridgeChain & BridgeAsset)[]>; /** @@ -156,27 +156,22 @@ export interface BridgeStatus { const bridgeAssetSchema = z.object({ /** - * The denomination of the asset. + * The displayable denomination of the asset. */ denom: z.string(), /** - * The address of the asset, represented as an IBC denom or EVM contract address. + * The address of the asset, represented as an IBC denom, origin denom, or EVM contract address. */ address: z.string(), /** * The number of decimal places for the asset. */ decimals: z.number(), - - /** - * Global identifier for denom on origin chain. - */ - sourceDenom: z.string(), }); export type BridgeAsset = z.infer; -const bridgeSupportedAssetsSchema = z.object({ +const getBridgeSupportedAssetsParams = z.object({ /** * The originating chain information. */ @@ -187,8 +182,8 @@ const bridgeSupportedAssetsSchema = z.object({ asset: bridgeAssetSchema, }); -export type BridgeSupportedAssetsParams = z.infer< - typeof bridgeSupportedAssetsSchema +export type GetBridgeSupportedAssetsParams = z.infer< + typeof getBridgeSupportedAssetsParams >; export interface BridgeDepositAddress { @@ -208,11 +203,14 @@ export interface GetDepositAddressParams { * The asset on the originating chain. */ fromAsset: BridgeAsset; + /** + * The asset on the destination chain. + */ + toAsset: BridgeAsset; /** * The address on the destination chain where the assets are to be received. */ toAddress: string; - autoUnwrapIntoNative?: boolean; } export const getBridgeExternalUrlSchema = z.object({ @@ -260,7 +258,7 @@ export const getBridgeQuoteSchema = z.object({ */ toAsset: bridgeAssetSchema, /** - * The amount to be transferred from the originating chain, represented as a string. + * The amount to be transferred from the originating chain, represented as a string integer. */ fromAmount: z.string(), /** @@ -317,8 +315,8 @@ export type BridgeTransactionRequest = export type BridgeCoin = { amount: string; denom: string; - /** Global identifier for denom on origin chain. */ - sourceDenom: string; + /** The address of the asset, represented as an IBC denom, origin denom, or EVM contract address. */ + address: string; decimals: number; }; @@ -333,7 +331,7 @@ export interface BridgeQuote { /** * The fee for the transfer. */ - transferFee: BridgeCoin; + transferFee: BridgeCoin & { chainId: number | string }; /** * The estimated time to execute the transfer, represented in seconds. */ diff --git a/packages/bridge/src/skip/__tests__/mocks.ts b/packages/bridge/src/skip/__tests__/mocks.ts new file mode 100644 index 0000000000..153ee62828 --- /dev/null +++ b/packages/bridge/src/skip/__tests__/mocks.ts @@ -0,0 +1,508 @@ +export const SkipAssets = { + chain_to_assets_map: { + "1": { + assets: [ + { + denom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + chain_id: "1", + origin_denom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + origin_chain_id: "1", + trace: "", + is_cw20: false, + is_evm: true, + is_svm: false, + symbol: "USDC", + name: "USD Coin", + logo_uri: + "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + decimals: 6, + token_contract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + coingecko_id: "usd-coin", + recommended_symbol: "USDC", + }, + { + denom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + chain_id: "1", + origin_denom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + origin_chain_id: "1", + trace: "", + is_cw20: false, + is_evm: true, + is_svm: false, + symbol: "WETH", + name: "Wrapped Ether", + logo_uri: + "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg", + decimals: 18, + token_contract: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + coingecko_id: "weth", + recommended_symbol: "WETH", + }, + ], + }, + "osmosis-1": { + assets: [ + { + denom: + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + chain_id: "osmosis-1", + origin_denom: "uusdc", + origin_chain_id: "noble-1", + trace: "transfer/channel-750", + is_cw20: false, + is_evm: false, + is_svm: false, + symbol: "USDC", + name: "USDC", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", + decimals: 6, + description: + "USDC is a fully collateralized US Dollar stablecoin developed by CENTRE, the open source project with Circle being the first of several forthcoming issuers.", + coingecko_id: "usd-coin", + recommended_symbol: "USDC", + }, + { + denom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + chain_id: "osmosis-1", + origin_denom: "weth-wei", + origin_chain_id: "axelar-dojo-1", + trace: "transfer/channel-208", + is_cw20: false, + is_evm: false, + is_svm: false, + symbol: "ETH", + name: "ETH", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/weth.png", + decimals: 18, + description: + "Ethereum (ETH) is a decentralized, open-source blockchain system featuring smart contract functionality. It's the native cryptocurrency of the Ethereum platform, often regarded as the second most popular digital currency after Bitcoin. Ethereum was proposed in late 2013 and development was crowdfunded in 2014, leading to its network going live on 30 July 2015.\n\nETH, as a digital currency, is used for a variety of purposes within the Ethereum ecosystem, including the execution of decentralized smart contracts and as a mode of payment. Unlike Bitcoin, Ethereum was designed to be a platform for applications that can operate without the need for intermediaries, using blockchain technology. This has made Ethereum a leading platform for various applications, including decentralized finance (DeFi), non-fungible tokens (NFTs), and more. Ethereum is constantly evolving, with a significant upgrade termed Ethereum 2.0, which aims to improve its scalability, security, and sustainability.", + coingecko_id: "axlweth", + recommended_symbol: "ETH.axl", + }, + ], + }, + "agoric-3": { + assets: [ + { + denom: + "ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9", + chain_id: "agoric-3", + origin_denom: "uusdc", + origin_chain_id: "noble-1", + trace: "transfer/channel-62", + is_cw20: false, + is_evm: false, + is_svm: false, + symbol: "USDC", + name: "USDC", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", + decimals: 6, + coingecko_id: "usd-coin", + recommended_symbol: "USDC", + }, + ], + }, + "archway-1": { + assets: [ + { + denom: + "ibc/43897B9739BD63E3A08A88191999C632E052724AB96BD4C74AE31375C991F48D", + chain_id: "archway-1", + origin_denom: "uusdc", + origin_chain_id: "noble-1", + trace: "transfer/channel-29", + is_cw20: false, + is_evm: false, + is_svm: false, + symbol: "USDC", + name: "USDC", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", + decimals: 6, + description: "Native Coin", + coingecko_id: "usd-coin", + recommended_symbol: "USDC", + }, + ], + }, + "noble-1": { + assets: [ + { + denom: "uusdc", + chain_id: "noble-1", + origin_denom: "uusdc", + origin_chain_id: "noble-1", + trace: "", + is_cw20: false, + is_evm: false, + is_svm: false, + symbol: "USDC", + name: "USDC", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", + decimals: 6, + description: "USD Coin", + coingecko_id: "usd-coin", + recommended_symbol: "USDC", + }, + ], + }, + }, +}; + +export const SkipChains = { + chains: [ + { + chain_name: "Ethereum", + chain_id: "1", + pfm_enabled: false, + cosmos_module_support: { authz: false, feegrant: false }, + supports_memo: false, + logo_uri: + "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/info/logo.png", + bech32_prefix: "", + fee_assets: [], + chain_type: "evm", + ibc_capabilities: { + cosmos_pfm: false, + cosmos_ibc_hooks: false, + cosmos_memo: false, + cosmos_autopilot: false, + }, + is_testnet: false, + }, + { + chain_name: "osmosis", + chain_id: "osmosis-1", + pfm_enabled: true, + cosmos_module_support: { authz: true, feegrant: false }, + supports_memo: true, + logo_uri: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/osmosis/chain.png", + bech32_prefix: "osmo", + fee_assets: [ + { + denom: "uosmo", + gas_price: { low: "0.0025", average: "0.025", high: "0.04" }, + }, + ], + chain_type: "cosmos", + ibc_capabilities: { + cosmos_pfm: true, + cosmos_ibc_hooks: true, + cosmos_memo: true, + cosmos_autopilot: false, + }, + is_testnet: false, + }, + { + chain_name: "agoric", + chain_id: "agoric-3", + pfm_enabled: false, + cosmos_module_support: { authz: true, feegrant: true }, + supports_memo: true, + logo_uri: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/agoric/chain.png", + bech32_prefix: "agoric", + fee_assets: [ + { + denom: "uist", + gas_price: { low: "0.0034", average: "0.007", high: "0.02" }, + }, + { + denom: "ubld", + gas_price: { low: "0.03", average: "0.05", high: "0.07" }, + }, + ], + chain_type: "cosmos", + ibc_capabilities: { + cosmos_pfm: false, + cosmos_ibc_hooks: false, + cosmos_memo: true, + cosmos_autopilot: false, + }, + is_testnet: false, + }, + { + chain_name: "archway", + chain_id: "archway-1", + pfm_enabled: false, + cosmos_module_support: { authz: true, feegrant: true }, + supports_memo: true, + logo_uri: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/archway/chain.png", + bech32_prefix: "archway", + fee_assets: [ + { + denom: "aarch", + gas_price: { + low: "140000000000", + average: "196000000000", + high: "225400000000", + }, + }, + ], + chain_type: "cosmos", + ibc_capabilities: { + cosmos_pfm: false, + cosmos_ibc_hooks: false, + cosmos_memo: true, + cosmos_autopilot: false, + }, + is_testnet: false, + }, + { + chain_name: "noble", + chain_id: "noble-1", + pfm_enabled: true, + cosmos_module_support: { authz: true, feegrant: true }, + supports_memo: true, + logo_uri: + "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/noble/chain.png", + bech32_prefix: "noble", + fee_assets: [ + { + denom: + "ibc/EF48E6B1A1A19F47ECAEA62F5670C37C0580E86A9E88498B7E393EB6F49F33C0", + gas_price: { low: "0.01", average: "0.01", high: "0.02" }, + }, + { + denom: "uusdc", + gas_price: { low: "0.1", average: "0.1", high: "0.2" }, + }, + ], + chain_type: "cosmos", + ibc_capabilities: { + cosmos_pfm: true, + cosmos_ibc_hooks: false, + cosmos_memo: true, + cosmos_autopilot: false, + }, + is_testnet: false, + }, + ], +}; + +export const ETH_OsmosisToEthereum_Route = { + source_asset_denom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + source_asset_chain_id: "osmosis-1", + dest_asset_denom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + dest_asset_chain_id: "1", + amount_in: "10000000000000000000", + amount_out: "9992274579512577377", + operations: [ + { + axelar_transfer: { + from_chain: "osmosis", + from_chain_id: "osmosis-1", + to_chain: "Ethereum", + to_chain_id: "1", + asset: "weth-wei", + should_unwrap: false, + denom_in: "weth-wei", + denom_out: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + fee_amount: "7725420487422623", + usd_fee_amount: "26.94", + fee_asset: { + denom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + chain_id: "1", + origin_denom: "", + origin_chain_id: "", + trace: "", + is_cw20: false, + is_evm: true, + is_svm: false, + symbol: "WETH", + name: "Wrapped Ether", + logo_uri: + "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg", + decimals: 18, + token_contract: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + }, + is_testnet: false, + ibc_transfer_to_axelar: { + port: "transfer", + channel: "channel-208", + from_chain_id: "osmosis-1", + to_chain_id: "axelar-dojo-1", + pfm_enabled: true, + supports_memo: true, + denom_in: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + denom_out: "weth-wei", + bridge_id: "IBC", + smart_relay: false, + chain_id: "osmosis-1", + dest_denom: "weth-wei", + }, + bridge_id: "AXELAR", + smart_relay: false, + }, + tx_index: 0, + amount_in: "10000000000000000000", + amount_out: "9992274579512577377", + }, + ], + chain_ids: ["osmosis-1", "1"], + does_swap: false, + estimated_amount_out: "9992274579512577377", + swap_venues: [], + txs_required: 1, + usd_amount_in: "34871.80", + usd_amount_out: "34844.86", + estimated_fees: [], + required_chain_addresses: ["osmosis-1", "1"], +}; + +export const ETH_EthereumToOsmosis_Route = { + source_asset_denom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + source_asset_chain_id: "1", + dest_asset_denom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + dest_asset_chain_id: "osmosis-1", + amount_in: "10000000000000000000", + amount_out: "10000000000000000000", + operations: [ + { + axelar_transfer: { + from_chain: "Ethereum", + from_chain_id: "1", + to_chain: "osmosis", + to_chain_id: "osmosis-1", + asset: "weth-wei", + should_unwrap: false, + denom_in: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + denom_out: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + fee_amount: "73924361079993", + usd_fee_amount: "0.26", + fee_asset: { + denom: "ethereum-native", + chain_id: "1", + origin_denom: "", + origin_chain_id: "", + trace: "", + is_cw20: false, + is_evm: true, + is_svm: false, + symbol: "ETH", + name: "Ethereum", + logo_uri: + "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/eth-blue.svg", + decimals: 18, + token_contract: "", + }, + is_testnet: false, + bridge_id: "AXELAR", + smart_relay: false, + }, + tx_index: 0, + amount_in: "10000000000000000000", + amount_out: "10000000000000000000", + }, + ], + chain_ids: ["1", "osmosis-1"], + does_swap: false, + estimated_amount_out: "10000000000000000000", + swap_venues: [], + txs_required: 1, + usd_amount_in: "34843.80", + usd_amount_out: "34843.80", + estimated_fees: [], + required_chain_addresses: ["1", "osmosis-1"], +}; + +export const ETH_OsmosisToEthereum_Msgs = { + msgs: [ + { + multi_chain_msg: { + chain_id: "osmosis-1", + path: ["osmosis-1", "1"], + msg: '{"source_port":"transfer","source_channel":"channel-208","token":{"denom":"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5","amount":"10000000000000000000"},"sender":"osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx","receiver":"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5","timeout_height":{},"timeout_timestamp":1718978568036848764,"memo":"{\\"destination_chain\\":\\"Ethereum\\",\\"destination_address\\":\\"0xD397883c12b71ea39e0d9f6755030205f31A1c96\\",\\"payload\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,120,99,236,5,177,35,136,92,118,9,176,92,53,223,119,127,63,24,2,88],\\"type\\":2,\\"fee\\":{\\"amount\\":\\"7725420487422623\\",\\"recipient\\":\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\"}}"}', + msg_type_url: "/ibc.applications.transfer.v1.MsgTransfer", + }, + }, + ], + txs: [ + { + cosmos_tx: { + chain_id: "osmosis-1", + path: ["osmosis-1", "1"], + msgs: [ + { + msg: '{"source_port":"transfer","source_channel":"channel-208","token":{"denom":"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5","amount":"10000000000000000000"},"sender":"osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx","receiver":"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5","timeout_height":{},"timeout_timestamp":1718978568036848764,"memo":"{\\"destination_chain\\":\\"Ethereum\\",\\"destination_address\\":\\"0xD397883c12b71ea39e0d9f6755030205f31A1c96\\",\\"payload\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,120,99,236,5,177,35,136,92,118,9,176,92,53,223,119,127,63,24,2,88],\\"type\\":2,\\"fee\\":{\\"amount\\":\\"7725420487422623\\",\\"recipient\\":\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\"}}"}', + msg_type_url: "/ibc.applications.transfer.v1.MsgTransfer", + }, + ], + signer_address: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + }, + operations_indices: [0], + }, + ], + estimated_fees: [ + { + fee_type: "SMART_RELAY", + bridge_id: "IBC", + amount: "", + usd_amount: "", + origin_asset: null, + chain_id: "", + tx_index: 0, + }, + ], +}; +export const ETH_EthereumToOsmosis_Msgs = { + msgs: [ + { + evm_tx: { + chain_id: "1", + to: "0xD397883c12b71ea39e0d9f6755030205f31A1c96", + value: "73924361079993", + data: "d421c10500000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000433bdb484cb900000000000000000000000000000000000000000000000000000000000000076f736d6f73697300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b6f736d6f313037767975657236777a6665376e727273756a7070613070767833356676706c7034743774780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000007b7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000", + required_erc20_approvals: [ + { + token_contract: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + spender: "0xD397883c12b71ea39e0d9f6755030205f31A1c96", + amount: "10000000000000000000", + }, + ], + signer_address: "0x7863Ec05b123885c7609B05c35Df777F3F180258", + }, + }, + ], + txs: [ + { + evm_tx: { + chain_id: "1", + to: "0xD397883c12b71ea39e0d9f6755030205f31A1c96", + value: "73924361079993", + data: "d421c10500000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000433bdb484cb900000000000000000000000000000000000000000000000000000000000000076f736d6f73697300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b6f736d6f313037767975657236777a6665376e727273756a7070613070767833356676706c7034743774780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000007b7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000", + required_erc20_approvals: [ + { + token_contract: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + spender: "0xD397883c12b71ea39e0d9f6755030205f31A1c96", + amount: "10000000000000000000", + }, + ], + signer_address: "0x7863Ec05b123885c7609B05c35Df777F3F180258", + }, + operations_indices: [0], + }, + ], + estimated_fees: [ + { + fee_type: "SMART_RELAY", + bridge_id: "IBC", + amount: "", + usd_amount: "", + origin_asset: null, + chain_id: "", + tx_index: 0, + }, + ], +}; diff --git a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts index 4c848596dc..40a4a3ce85 100644 --- a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts +++ b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts @@ -7,7 +7,6 @@ import { rest } from "msw"; import { MockAssetLists } from "../../__tests__/mock-asset-lists"; import { server } from "../../__tests__/msw"; import { - BridgeAsset, BridgeChain, BridgeProviderContext, BridgeTransactionRequest, @@ -16,6 +15,14 @@ import { } from "../../interface"; import { SkipBridgeProvider } from ".."; import { SkipMsg } from "../types"; +import { + ETH_EthereumToOsmosis_Msgs, + ETH_EthereumToOsmosis_Route, + ETH_OsmosisToEthereum_Msgs, + ETH_OsmosisToEthereum_Route, + SkipAssets, + SkipChains, +} from "./mocks"; jest.mock("viem", () => ({ ...jest.requireActual("viem"), @@ -41,209 +48,11 @@ jest.mock("@cosmjs/proto-signing", () => ({ beforeEach(() => { server.use( - rest.get("https://api.skip.money/v1/fungible/assets", (_req, res, ctx) => { - return res( - ctx.json({ - chain_to_assets_map: { - "1": { - assets: [ - { - denom: "asset1", - chain_id: "1", - origin_denom: "asset1", - origin_chain_id: "1", - trace: "", - is_cw20: false, - symbol: "AS1", - name: "Asset 1", - logo_uri: "http://example.com/logo1.png", - decimals: 18, - token_contract: "0x123", - description: "Description of asset1", - coingecko_id: "asset1", - recommended_symbol: "AS1", - }, - { - denom: "asset2", - chain_id: "1", - origin_denom: "asset2", - origin_chain_id: "1", - trace: "", - is_cw20: false, - symbol: "AS2", - name: "Asset 2", - logo_uri: "http://example.com/logo2.png", - decimals: 18, - token_contract: "0x456", - description: "Description of asset2", - coingecko_id: "asset2", - recommended_symbol: "AS2", - }, - { - denom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - chain_id: "1", - origin_denom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - origin_chain_id: "1", - trace: "", - is_cw20: false, - is_evm: true, - is_svm: false, - symbol: "USDC", - name: "USD Coin", - logo_uri: - "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", - decimals: 6, - token_contract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - coingecko_id: "usd-coin", - recommended_symbol: "USDC", - }, - ], - }, - "osmosis-1": { - assets: [ - { - denom: - "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", - chain_id: "osmosis-1", - origin_denom: "uusdc", - origin_chain_id: "noble-1", - trace: "transfer/channel-750", - is_cw20: false, - is_evm: false, - is_svm: false, - symbol: "USDC", - name: "USDC", - logo_uri: - "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", - decimals: 6, - description: - "USDC is a fully collateralized US Dollar stablecoin developed by CENTRE, the open source project with Circle being the first of several forthcoming issuers.", - coingecko_id: "usd-coin", - recommended_symbol: "USDC", - }, - ], - }, - "agoric-3": { - assets: [ - { - denom: - "ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9", - chain_id: "agoric-3", - origin_denom: "uusdc", - origin_chain_id: "noble-1", - trace: "transfer/channel-62", - is_cw20: false, - is_evm: false, - is_svm: false, - symbol: "USDC", - name: "USDC", - logo_uri: - "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", - decimals: 6, - coingecko_id: "usd-coin", - recommended_symbol: "USDC", - }, - ], - }, - "archway-1": { - assets: [ - { - denom: - "ibc/43897B9739BD63E3A08A88191999C632E052724AB96BD4C74AE31375C991F48D", - chain_id: "archway-1", - origin_denom: "uusdc", - origin_chain_id: "noble-1", - trace: "transfer/channel-29", - is_cw20: false, - is_evm: false, - is_svm: false, - symbol: "USDC", - name: "USDC", - logo_uri: - "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", - decimals: 6, - description: "Native Coin", - coingecko_id: "usd-coin", - recommended_symbol: "USDC", - }, - ], - }, - "noble-1": { - assets: [ - { - denom: "uusdc", - chain_id: "noble-1", - origin_denom: "uusdc", - origin_chain_id: "noble-1", - trace: "", - is_cw20: false, - is_evm: false, - is_svm: false, - symbol: "USDC", - name: "USDC", - logo_uri: - "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png", - decimals: 6, - description: "USD Coin", - coingecko_id: "usd-coin", - recommended_symbol: "USDC", - }, - ], - }, - }, - }) - ); - }), - rest.get("https://api.skip.money/v1/info/chains", (_req, res, ctx) => { - return res( - ctx.json({ - chains: [ - { - chain_name: "Ethereum", - chain_id: "1", - pfm_enabled: true, - cosmos_sdk_version: "0.42.0", - supports_memo: true, - logo_uri: "http://example.com/eth.png", - bech32_prefix: "cosmos", - chain_type: "evm", - }, - ], - }) - ); - }), - rest.post("https://api.skip.money/v2/fungible/route", (_req, res, ctx) => { - return res( - ctx.json({ - source_asset_denom: "asset1", - source_asset_chain_id: "1", - dest_asset_denom: "asset2", - dest_asset_chain_id: "1", - amount_in: "1000", - amount_out: "1000", - operations: [], - chain_ids: ["1"], - does_swap: false, - txs_required: 1, - }) - ); + rest.get("https://api.skip.money/v2/fungible/assets", (_req, res, ctx) => { + return res(ctx.json(SkipAssets)); }), - rest.post("https://api.skip.money/v2/fungible/msgs", (_req, res, ctx) => { - return res( - ctx.json({ - msgs: [ - { - evm_tx: { - chain_id: "1", - to: "0x123", - value: "1000", - data: "abcdef", - required_erc20_approvals: [], - }, - }, - ], - }) - ); + rest.get("https://api.skip.money/v2/info/chains", (_req, res, ctx) => { + return res(ctx.json(SkipChains)); }) ); jest.clearAllMocks(); @@ -270,66 +79,191 @@ describe("SkipBridgeProvider", () => { provider = new SkipBridgeProvider(ctx); }); - it("should get a quote", async () => { - const params: GetBridgeQuoteParams = { - fromAmount: "1000", + it("should get a quote - ETH.axl from Osmosis to Ethereum", async () => { + server.use( + rest.post("https://api.skip.money/v2/fungible/route", (_req, res, ctx) => + res(ctx.json(ETH_OsmosisToEthereum_Route)) + ), + rest.post("https://api.skip.money/v2/fungible/msgs", (_req, res, ctx) => + res(ctx.json(ETH_OsmosisToEthereum_Msgs)) + ) + ); + + // Mock gas fee estimation of IBC transfer + (estimateGasFee as jest.Mock).mockResolvedValue({ + gas: "420000", + amount: [ + { + denom: "uosmo", + amount: "1232", + }, + ], + }); + + const quote = await provider.getQuote({ + fromAmount: "10000000000000000000", fromAsset: { - denom: "asset1", - address: "0x123", + denom: "ETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, - sourceDenom: "asset1", }, - fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, + fromChain: { + chainId: "osmosis-1", + chainName: "osmosis", + chainType: "cosmos", + }, toAsset: { - denom: "asset2", - address: "0x456", + denom: "WETH", + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", decimals: 18, - sourceDenom: "asset2", }, toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, - fromAddress: "0xabc", - toAddress: "0xdef", + fromAddress: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + toAddress: "0x7863Ec05b123885c7609B05c35Df777F3F180258", slippage: 0.01, - }; + }); + + expect(quote).toBeDefined(); + expect(quote).toEqual({ + input: { + amount: "10000000000000000000", + denom: "ETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + }, + expectedOutput: { + amount: "9992274579512577377", + denom: "WETH", + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + decimals: 18, + priceImpact: "0", + }, + fromChain: { + chainId: "osmosis-1", + chainName: "osmosis", + chainType: "cosmos", + }, + toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, + transferFee: { + amount: "7725420487422623", + denom: "WETH", + chainId: 1, + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + }, + estimatedTime: 960, + transactionRequest: { + type: "cosmos", + msgTypeUrl: "/ibc.applications.transfer.v1.MsgTransfer", + msg: { + sourcePort: "transfer", + sourceChannel: "channel-208", + token: { + denom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + amount: "10000000000000000000", + }, + sender: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + receiver: + "axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5", + timeoutHeight: { + revisionNumber: "1", + revisionHeight: "1000", + }, + timeoutTimestamp: "0", + memo: '{"destination_chain":"Ethereum","destination_address":"0xD397883c12b71ea39e0d9f6755030205f31A1c96","payload":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,120,99,236,5,177,35,136,92,118,9,176,92,53,223,119,127,63,24,2,88],"type":2,"fee":{"amount":"7725420487422623","recipient":"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89"}}', + }, + }, + estimatedGasFee: { + amount: "1232", + denom: "OSMO", + decimals: 6, + address: "uosmo", + }, + }); + }); - const quote = await provider.getQuote(params); + it("should get a quote - ETH.axl from Ethereum to Osmosis", async () => { + server.use( + rest.post("https://api.skip.money/v2/fungible/route", (_req, res, ctx) => + res(ctx.json(ETH_EthereumToOsmosis_Route)) + ), + rest.post("https://api.skip.money/v2/fungible/msgs", (_req, res, ctx) => + res(ctx.json(ETH_EthereumToOsmosis_Msgs)) + ) + ); + + const quote = await provider.getQuote({ + fromAmount: "10000000000000000000", + toAsset: { + denom: "ETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + }, + toChain: { + chainId: "osmosis-1", + chainName: "osmosis", + chainType: "cosmos", + }, + fromAsset: { + denom: "WETH", + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + decimals: 18, + }, + fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, + toAddress: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + fromAddress: "0x7863Ec05b123885c7609B05c35Df777F3F180258", + slippage: 0.01, + }); expect(quote).toBeDefined(); expect(quote).toEqual({ input: { - amount: "1000", - denom: "asset1", - sourceDenom: "asset1", + amount: "10000000000000000000", + denom: "WETH", + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", decimals: 18, }, expectedOutput: { - amount: "1000", - denom: "asset2", - sourceDenom: "asset2", + amount: "10000000000000000000", + denom: "ETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, priceImpact: "0", }, + toChain: { + chainId: "osmosis-1", + chainName: "osmosis", + chainType: "cosmos", + }, fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, - toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, transferFee: { - amount: "0", - denom: "asset1", - sourceDenom: "asset1", + amount: "73924361079993", + denom: "ETH", + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, }, estimatedTime: 960, transactionRequest: { type: "evm", - to: "0x123", - data: "0xabcdef", - value: "0x3e8", - approvalTransactionRequest: undefined, + to: "0xD397883c12b71ea39e0d9f6755030205f31A1c96", + data: "0xd421c10500000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000433bdb484cb900000000000000000000000000000000000000000000000000000000000000076f736d6f73697300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b6f736d6f313037767975657236777a6665376e727273756a7070613070767833356676706c7034743774780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000007b7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000", + value: "0x433bdb484cb9", + approvalTransactionRequest: { + to: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + data: "0xabcdef", + }, }, estimatedGasFee: { amount: "420000000000000", - decimals: 18, denom: "ETH", - sourceDenom: "ETH", + decimals: 18, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", }, }); }); @@ -337,7 +271,7 @@ describe("SkipBridgeProvider", () => { it("should handle unsupported asset error", async () => { server.use( rest.get( - "https://api.skip.money/v1/fungible/assets", + "https://api.skip.money/v2/fungible/assets", (_req, res, ctx) => { return res( ctx.json({ @@ -356,14 +290,12 @@ describe("SkipBridgeProvider", () => { denom: "asset1", address: "0x123", decimals: 18, - sourceDenom: "asset1", }, fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toAsset: { denom: "asset2", address: "0x456", decimals: 18, - sourceDenom: "asset2", }, toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, fromAddress: "0xabc", @@ -409,14 +341,12 @@ describe("SkipBridgeProvider", () => { denom: "asset1", address: "0x123", decimals: 18, - sourceDenom: "asset1", }, fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toAsset: { denom: "asset2", address: "0x456", decimals: 18, - sourceDenom: "asset2", }, toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, fromAddress: "0xabc", @@ -445,7 +375,6 @@ describe("SkipBridgeProvider", () => { denom: "asset1", address: "ibc/123", decimals: 6, - sourceDenom: "asset1", }, fromChain: { chainId: "osmosis-1", @@ -456,7 +385,6 @@ describe("SkipBridgeProvider", () => { denom: "asset2", address: "0x456", decimals: 6, - sourceDenom: "asset2", }, toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, fromAddress: "osmo1ABC123", @@ -493,26 +421,25 @@ describe("SkipBridgeProvider", () => { expect(gasCost).toBeDefined(); expect(gasCost?.amount).toBe("1000"); expect(gasCost?.denom).toBe("OSMO"); - expect(gasCost?.sourceDenom).toBe("uosmo"); + expect(gasCost?.address).toBe("uosmo"); }); it("should fetch and return the correct skip asset", async () => { - const chain: BridgeChain = { - chainId: 1, - chainName: "Ethereum", - chainType: "evm", - }; - const asset: BridgeAsset = { - denom: "asset1", - address: "0x123", - decimals: 18, - sourceDenom: "asset1", - }; - - const skipAsset = await provider.getAsset(chain, asset); + const skipAsset = await provider.getAsset( + { + chainId: 1, + chainName: "Ethereum", + chainType: "evm", + }, + { + denom: "USDC", + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals: 6, + } + ); expect(skipAsset).toBeDefined(); - expect(skipAsset?.denom).toBe("asset1"); + expect(skipAsset?.denom).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); }); it("should fetch and cache skip assets", async () => { @@ -618,7 +545,6 @@ describe("SkipBridgeProvider", () => { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -629,7 +555,6 @@ describe("SkipBridgeProvider", () => { chainType: "cosmos", decimals: 6, denom: "USDC", - sourceDenom: "uusdc", }, { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", @@ -637,7 +562,6 @@ describe("SkipBridgeProvider", () => { chainType: "evm", decimals: 6, denom: "USDC", - sourceDenom: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", }, { address: @@ -645,7 +569,6 @@ describe("SkipBridgeProvider", () => { chainId: "agoric-3", chainType: "cosmos", denom: "USDC", - sourceDenom: "uusdc", decimals: 6, }, { @@ -654,7 +577,6 @@ describe("SkipBridgeProvider", () => { chainId: "archway-1", chainType: "cosmos", denom: "USDC", - sourceDenom: "uusdc", decimals: 6, }, ]); @@ -671,7 +593,6 @@ describe("SkipBridgeProvider", () => { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -682,7 +603,6 @@ describe("SkipBridgeProvider", () => { chainType: "cosmos", decimals: 6, denom: "USDC", - sourceDenom: "uusdc", }); }); @@ -697,7 +617,6 @@ describe("SkipBridgeProvider", () => { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -708,7 +627,6 @@ describe("SkipBridgeProvider", () => { chainType: "evm", decimals: 6, denom: "USDC", - sourceDenom: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", }); }); }); @@ -745,13 +663,11 @@ describe("SkipBridgeProvider.getExternalUrl", () => { address: "uatom", denom: "uatom", decimals: 6, - sourceDenom: "uatom", }, toAsset: { address: "ubld", denom: "ubld", decimals: 6, - sourceDenom: "ubld", }, toAddress: "cosmos1...", }); @@ -771,16 +687,12 @@ describe("SkipBridgeProvider.getExternalUrl", () => { "ibc/2e5d0ac026ac1afa65a23023ba4f24bb8ddf94f118edc0bad6f625bfc557cded", denom: "AKT", decimals: 6, - sourceDenom: - "ibc/2e5d0ac026ac1afa65a23023ba4f24bb8ddf94f118edc0bad6f625bfc557cded", }, toAsset: { address: "ibc/976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97", denom: "ANDR", decimals: 6, - sourceDenom: - "ibc/976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97", }, toAddress: "cosmos1...", }); @@ -799,14 +711,12 @@ describe("SkipBridgeProvider.getExternalUrl", () => { address: "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", decimals: 18, denom: "USDC", - sourceDenom: "USDC", }, toAsset: { address: "ibc/976c73350f6f48a69de740784c8a92931c696581a5c720d96ddf4afa860fff97", decimals: 18, denom: "USDC", - sourceDenom: "USDC", }, toAddress: "cosmos1...", }); diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index af8103cc20..e27f4da8d1 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -1,6 +1,5 @@ import { fromBech32, toBech32 } from "@cosmjs/encoding"; import { Registry } from "@cosmjs/proto-signing"; -import { CoinPretty } from "@keplr-wallet/unit"; import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; @@ -28,12 +27,12 @@ import { BridgeProvider, BridgeProviderContext, BridgeQuote, - BridgeSupportedAssetsParams, BridgeTransactionRequest, CosmosBridgeTransactionRequest, EvmBridgeTransactionRequest, GetBridgeExternalUrlParams, GetBridgeQuoteParams, + GetBridgeSupportedAssetsParams, } from "../interface"; import { cosmosMsgOpts } from "../msg"; import { BridgeAssetMap } from "../utils"; @@ -76,11 +75,13 @@ export class SkipBridgeProvider implements BridgeProvider { toChain, slippage, }), + ttl: process.env.NODE_ENV === "test" ? -1 : 20 * 1000, // 20 seconds getFreshValue: async (): Promise => { const sourceAsset = await this.getAsset(fromChain, fromAsset); if (!sourceAsset) { throw new BridgeQuoteError({ + bridgeId: SkipBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: `Unsupported asset ${fromAsset.denom} on ${fromChain.chainName}`, }); @@ -90,6 +91,7 @@ export class SkipBridgeProvider implements BridgeProvider { if (!destinationAsset) { throw new BridgeQuoteError({ + bridgeId: SkipBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: `Unsupported asset ${toAsset.denom} on ${toChain.chainName}`, }); @@ -111,42 +113,27 @@ export class SkipBridgeProvider implements BridgeProvider { toChain ); - const amountOut = new CoinPretty( - { - coinDecimals: toAsset.decimals, - coinDenom: toAsset.denom, - coinMinimalDenom: toAsset.denom, - }, - route.amount_out - ); - - let transferFee: BridgeCoin = { + let transferFee: BridgeCoin & { chainId: number | string } = { + ...fromAsset, amount: "0", - denom: fromAsset.denom, - sourceDenom: fromAsset.sourceDenom, - decimals: fromAsset.decimals, + chainId: fromChain.chainId, }; for (const operation of route.operations) { if ("axelar_transfer" in operation) { - const feeAmount = new CoinPretty( - { - coinDecimals: operation.axelar_transfer.fee_asset.decimals ?? 6, - coinDenom: - operation.axelar_transfer.fee_asset.symbol ?? - operation.axelar_transfer.fee_asset.denom, - coinMinimalDenom: operation.axelar_transfer.fee_asset.denom, - }, - operation.axelar_transfer.fee_amount - ); + const feeAsset = operation.axelar_transfer.fee_asset; transferFee = { - amount: feeAmount.toCoin().amount, - denom: - operation.axelar_transfer.fee_asset.symbol ?? - operation.axelar_transfer.fee_asset.denom, - sourceDenom: operation.axelar_transfer.fee_asset.denom, - decimals: operation.axelar_transfer.fee_asset.decimals ?? 6, + amount: operation.axelar_transfer.fee_amount, + denom: feeAsset.symbol ?? feeAsset.denom, + chainId: feeAsset.is_evm + ? Number(feeAsset.chain_id) + : feeAsset.chain_id, + address: + feeAsset.is_evm && !Boolean(feeAsset.token_contract) + ? NativeEVMTokenConstantAddress + : feeAsset.token_contract!, + decimals: feeAsset.decimals ?? 6, }; } } @@ -179,10 +166,10 @@ export class SkipBridgeProvider implements BridgeProvider { destinationAsset.chain_id ); - const estimatedTime = - sourceChainFinalityTime > destinationChainFinalityTime - ? sourceChainFinalityTime - : destinationChainFinalityTime; + const estimatedTime = Math.max( + sourceChainFinalityTime, + destinationChainFinalityTime + ); const estimatedGasFee = await this.estimateGasCost( params, @@ -191,16 +178,12 @@ export class SkipBridgeProvider implements BridgeProvider { return { input: { + ...fromAsset, amount: fromAmount, - denom: fromAsset.denom, - sourceDenom: fromAsset.sourceDenom, - decimals: fromAsset.decimals, }, expectedOutput: { - amount: amountOut.toCoin().amount, - denom: toAsset.denom, - sourceDenom: toAsset.sourceDenom, - decimals: toAsset.decimals, + amount: route.amount_out, + ...toAsset, priceImpact: "0", }, fromChain, @@ -211,7 +194,6 @@ export class SkipBridgeProvider implements BridgeProvider { estimatedGasFee, }; }, - ttl: 20 * 1000, // 20 seconds, }); } @@ -224,7 +206,7 @@ export class SkipBridgeProvider implements BridgeProvider { async getSupportedAssets({ chain, asset, - }: BridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { + }: GetBridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { try { const chainAsset = await this.getAsset(chain, asset); if (!chainAsset) throw new Error("Asset not found: " + asset.address); @@ -271,7 +253,6 @@ export class SkipBridgeProvider implements BridgeProvider { address: c.sourceDenom, denom: c.symbol, decimals: c.decimals, - sourceDenom: c.sourceDenom, }); } } @@ -290,7 +271,6 @@ export class SkipBridgeProvider implements BridgeProvider { address: c.sourceDenom, denom: c.symbol, decimals: c.decimals, - sourceDenom: c.sourceDenom, }); } } @@ -335,7 +315,6 @@ export class SkipBridgeProvider implements BridgeProvider { sharedOriginAsset.name ?? sharedOriginAsset.denom, decimals: sharedOriginAsset.decimals ?? asset.decimals, - sourceDenom: sharedOriginAsset.origin_denom, } ); } @@ -642,7 +621,10 @@ export class SkipBridgeProvider implements BridgeProvider { ({ id: chainId }) => chainId === params.fromChain.chainId ); - if (!evmChain) throw new Error("Could not find EVM chain"); + if (!evmChain) + throw new Error( + "Could not find EVM chain: " + params.fromChain.chainId + ); const provider = createPublicClient({ chain: evmChain, @@ -668,9 +650,9 @@ export class SkipBridgeProvider implements BridgeProvider { return { amount: gasCost.toString(), - sourceDenom: evmChain.nativeCurrency.symbol, - decimals: evmChain.nativeCurrency.decimals, denom: evmChain.nativeCurrency.symbol, + decimals: evmChain.nativeCurrency.decimals, + address: NativeEVMTokenConstantAddress, }; } @@ -698,7 +680,7 @@ export class SkipBridgeProvider implements BridgeProvider { amount: gasFee.amount, denom: gasAsset?.symbol ?? gasFee.denom, decimals: gasAsset?.decimals ?? 0, - sourceDenom: gasAsset?.coinMinimalDenom ?? gasFee.denom, + address: gasAsset?.coinMinimalDenom ?? gasFee.denom, }; } } @@ -710,53 +692,54 @@ export class SkipBridgeProvider implements BridgeProvider { ) { try { if (!txData.approvalTransactionRequest) { - const estimatedGas = await provider.estimateGas({ - account: params.fromAddress as Address, - to: txData.to, - data: txData.data, - value: !isNil(txData.value) ? BigInt(txData.value) : undefined, - }); - - return BigInt(estimatedGas); + return await provider + .estimateGas({ + account: params.fromAddress as Address, + to: txData.to, + data: txData.data, + value: !isNil(txData.value) ? BigInt(txData.value) : undefined, + }) + .then((gas) => BigInt(gas)); } - const slot = 10; // Allowance slot (differs from contract to contract but is usually 10) + // Adding a stateDiff override allows us to estimate the gas without the user having first approved the ERC20 transfer + // Otherwise, the estimate call would fail with an error indicating the user has not approved the transfer + + /* Allowance slot (differs from contract to contract but is usually 10) */ + const slot = 10; - const temp = keccak256( + const erc20Balance = keccak256( encodePacked( ["uint256", "uint256"], [BigInt(params.fromAddress), BigInt(slot)] ) ); const index = keccak256( - encodePacked(["uint256", "uint256"], [BigInt(txData.to), BigInt(temp)]) + encodePacked( + ["uint256", "uint256"], + [BigInt(txData.to), BigInt(erc20Balance)] + ) ); - const callParams = [ - { - from: params.fromAddress, + return await provider + .estimateGas({ + account: params.fromAddress as Address, to: txData.to, data: txData.data, - value: txData.value, - }, - "latest", - ]; - - const stateDiff = { - [txData.approvalTransactionRequest.to]: { - stateDiff: { - [index]: `0x${maxUint256.toString(16)}`, - }, - }, - }; - - // Call with no state overrides - const callResult = await provider.request({ - method: "eth_estimateGas", - params: [...callParams, stateDiff], - }); - - return BigInt(callResult); + value: !isNil(txData.value) ? BigInt(txData.value) : undefined, + stateOverride: [ + { + address: txData.approvalTransactionRequest.to as Address, + stateDiff: [ + { + slot: index, + value: `0x${maxUint256.toString(16)}`, + }, + ], + }, + ], + }) + .then((gas) => BigInt(gas)); } catch (err) { console.error("failed to estimate gas:", err); return BigInt(0); diff --git a/packages/bridge/src/skip/queries.ts b/packages/bridge/src/skip/queries.ts index 8a5dc3137e..d89f7a31ba 100644 --- a/packages/bridge/src/skip/queries.ts +++ b/packages/bridge/src/skip/queries.ts @@ -18,7 +18,7 @@ export class SkipApiClient { ) {} async assets({ chainID }: { chainID?: string } = {}) { - const url = new URL("/v1/fungible/assets", this.baseUrl); + const url = new URL("/v2/fungible/assets", this.baseUrl); url.searchParams.append("include_evm_assets", "true"); url.searchParams.append("include_cw20_assets", "true"); @@ -39,7 +39,7 @@ export class SkipApiClient { } async chains() { - const url = new URL("/v1/info/chains", this.baseUrl); + const url = new URL("/v2/info/chains", this.baseUrl); url.searchParams.append("include_evm", "true"); diff --git a/packages/bridge/src/squid/__tests__/mocks.ts b/packages/bridge/src/squid/__tests__/mocks.ts index e63cb26afb..3e1aed09a8 100644 --- a/packages/bridge/src/squid/__tests__/mocks.ts +++ b/packages/bridge/src/squid/__tests__/mocks.ts @@ -4231,3 +4231,519 @@ export const MockChains = [ estimatedExpressRouteDuration: 20, }, ]; + +export const ETHtoAVAX_EthereumToAvalanche_Route = { + route: { + estimate: { + fromAmount: "1000000000000000", + sendAmount: "1505469", + toAmount: "54602787339179287", + toAmountMin: "54056759465787494", + fromAmountUSD: "3.4995", + route: { + fromChain: [ + { + type: "SWAP", + dex: { + chainName: "Ethereum", + dexName: "Pancakeswap_v3", + swapRouter: "0x1b81D678ffb9C0263b24A97847620C99d213eB14", + quoter: "0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997", + isCrypto: true, + isStable: false, + }, + target: "0x1b81D678ffb9C0263b24A97847620C99d213eB14", + path: [ + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + ], + poolFees: [100], + swapType: "crypto", + squidCallType: 0, + fromToken: { + chainId: 1, + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + name: "Wrapped ETH", + symbol: "WETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + }, + toToken: { + name: "USDCoin", + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + decimals: 6, + chainId: 1, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "usd-coin", + commonKey: "uusdc", + }, + fromAmount: "1000000000000000", + toAmount: "3505469", + toAmountMin: "3498411", + exchangeRate: "3505.469", + priceImpact: "-0.0019950652992283", + isFrom: true, + dynamicSlippage: 0.201341386156495, + }, + ], + toChain: [ + { + type: "SWAP", + dex: { + chainName: "Avalanche", + dexName: "Curve_v2", + swapRouter: "0xBff334F8D5912AC5c4f2c590A2396d1C5d990123", + isStable: true, + isCrypto: true, + }, + target: "0xBff334F8D5912AC5c4f2c590A2396d1C5d990123", + path: [ + "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + ], + swapType: "stable", + squidCallType: 0, + fromToken: { + chainId: 43114, + address: "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", + name: "Axelar USDC", + symbol: "axlUSDC", + decimals: 6, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "axlusdc", + commonKey: "uusdc", + }, + toToken: { + chainId: 43114, + address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + decimals: 6, + name: "USD Coin", + symbol: "USDC", + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "usd-coin-avalanche-bridged-usdc-e", + commonKey: "avalanche-uusdc", + }, + fromAmount: "2000000", + toAmount: "2000206", + toAmountMin: "1992178", + exchangeRate: "1.000103", + priceImpact: "-0.0002899609648683", + isFrom: false, + dynamicSlippage: 0.401341386156495, + }, + { + type: "SWAP", + dex: { + chainName: "Avalanche", + dexName: "TraderJoe", + swapRouter: "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + factory: "0x9ad6c38be94206ca50bb0d90783181662f0cfa10", + isCrypto: true, + }, + target: "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + path: [ + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + ], + swapType: "crypto", + squidCallType: 1, + fromToken: { + chainId: 43114, + address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + decimals: 6, + name: "USD Coin", + symbol: "USDC", + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "usd-coin-avalanche-bridged-usdc-e", + commonKey: "avalanche-uusdc", + }, + toToken: { + chainId: 43114, + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + name: "Wrapped AVAX", + symbol: "WAVAX", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/avax.svg", + coingeckoId: "wrapped-avax", + commonKey: "wavax-wei", + }, + fromAmount: "2000206", + toAmount: "72539144530944138", + toAmountMin: "72101963603672917", + exchangeRate: "0.036265836884272988", + priceImpact: "0.0005375850513368", + isFrom: false, + dynamicSlippage: 0.60268277231299, + }, + { + type: "SWAP", + dex: { + chainName: "Avalanche", + dexName: "Curve_v2", + swapRouter: "0xBff334F8D5912AC5c4f2c590A2396d1C5d990123", + isStable: true, + isCrypto: true, + }, + target: "0xBff334F8D5912AC5c4f2c590A2396d1C5d990123", + path: [ + "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + ], + swapType: "stable", + squidCallType: 1, + fromToken: { + chainId: 43114, + address: "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", + name: "Axelar USDC", + symbol: "axlUSDC", + decimals: 6, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "axlusdc", + commonKey: "uusdc", + }, + toToken: { + chainId: 43114, + address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + decimals: 6, + name: "USD Coin", + symbol: "USDC", + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "usd-coin-avalanche-bridged-usdc-e", + commonKey: "avalanche-uusdc", + }, + fromAmount: "1505469", + toAmount: "1505624", + toAmountMin: "1493539", + exchangeRate: "1.000102957948652546", + priceImpact: "-0.0002857562510152", + isFrom: false, + dynamicSlippage: 0.8026827723129899, + }, + { + type: "SWAP", + dex: { + chainName: "Avalanche", + dexName: "TraderJoe", + swapRouter: "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + factory: "0x9ad6c38be94206ca50bb0d90783181662f0cfa10", + isCrypto: true, + }, + target: "0x60aE616a2155Ee3d9A68541Ba4544862310933d4", + path: [ + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + ], + swapType: "crypto", + squidCallType: 1, + fromToken: { + chainId: 43114, + address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + decimals: 6, + name: "USD Coin", + symbol: "USDC", + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + coingeckoId: "usd-coin-avalanche-bridged-usdc-e", + commonKey: "avalanche-uusdc", + }, + toToken: { + chainId: 43114, + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + name: "Wrapped AVAX", + symbol: "WAVAX", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/avax.svg", + coingeckoId: "wrapped-avax", + commonKey: "wavax-wei", + }, + fromAmount: "1505624", + toAmount: "54602787339179287", + toAmountMin: "54056759465787494", + exchangeRate: "0.036265885333376252", + priceImpact: "0.0004039914354954", + isFrom: false, + dynamicSlippage: 1, + }, + ], + }, + feeCosts: [ + { + name: "Gas Receiver Fee", + description: "Estimated Gas Receiver fee", + percentage: "0", + token: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg", + coingeckoId: "ethereum", + commonKey: "weth-wei", + }, + amount: "551234033843310", + amountUSD: "1.9276", + }, + ], + gasCosts: [ + { + type: "executeCall", + token: { + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg", + coingeckoId: "ethereum", + commonKey: "weth-wei", + }, + amount: "6259874503623000", + amountUSD: "21.8903", + gasPrice: "7585449207", + maxFeePerGas: "16668898414", + maxPriorityFeePerGas: "1500000000", + estimate: "689000", + limit: "689000", + }, + ], + estimatedRouteDuration: 960, + isExpressSupported: true, + exchangeRate: "54.602787339179287", + aggregatePriceImpact: "0.0", + toAmountUSD: "1.507", + toAmountMinUSD: "1.492", + }, + params: { + collectFees: { + feeLocation: "NONE", + }, + receiveGasOnDestination: true, + enableExpress: false, + slippage: 1, + quoteOnly: false, + toAddress: "0x6c515B41bFBEe0aA754F306098Ba005152c928b9", + fromAddress: "0x7863Ec05b123885c7609B05c35Df777F3F180258", + fromAmount: "1000000000000000", + toToken: { + chainId: 43114, + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + name: "Wrapped AVAX", + symbol: "WAVAX", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/avax.svg", + coingeckoId: "wrapped-avax", + commonKey: "wavax-wei", + }, + fromToken: { + chainId: 1, + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + name: "Wrapped ETH", + symbol: "WETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + }, + toChain: "43114", + fromChain: "1", + }, + transactionRequest: { + routeType: "CALL_BRIDGE_CALL", + targetAddress: "0xce16F69375520ab01377ce7B88f5BA8C48F8D666", + data: "0x846a1bc6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000054000000000000000000000000000000000000000000000000000000000000005a00000000000000000000000007863ec05b123885c7609b05c35df777f3f18025800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000001b81d678ffb9c0263b24a97847620c99d213eb1400000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b81d678ffb9c0263b24a97847620c99d213eb14000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000104414bf389000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000001903b771a8400000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000003561ab00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000094176616c616e6368650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30786365313646363933373535323061623031333737636537423838663542413843343846384436363600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164000000000000000000000000000000000000000000000000000000000000000400000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b900000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000082000000000000000000000000000000000000000000000000000000000000009a00000000000000000000000000000000000000000000000000000000000000be00000000000000000000000000000000000000000000000000000000000000d60000000000000000000000000000000000000000000000000000000000000122000000000000000000000000000000000000000000000000000000000000013a000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d9901230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d990123000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000003840651cb35000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000d7bb79aee866672419999a0496d99c54741d67b5000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e848000000000000000000000000000000000000000000000000000000000001e65f20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000104676528d100000000000000000000000000000000000000000000000000000000001e854e0000000000000000000000000000000000000000000000000100285a98347f5500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b9000000000000000000000000000000000000000000000000000001903b771a880000000000000000000000000000000000000000000000000000000000000002000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000b31f66aa3c1e785363f0875a1b74e27b85fd66c7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d9901230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d990123000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000003840651cb35000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000d7bb79aee866672419999a0496d99c54741d67b5000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016f8bd000000000000000000000000000000000000000000000000000000000016ca230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000010438ed1739000000000000000000000000000000000000000000000000000000000016f95800000000000000000000000000000000000000000000000000c00c5619aac06600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b9000000000000000000000000000000000000000000000000000001903b771a8c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000b31f66aa3c1e785363f0875a1b74e27b85fd66c7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000", + value: "551234033843310", + gasLimit: "689000", + gasPrice: "7585449207", + maxFeePerGas: "16668898414", + maxPriorityFeePerGas: "1500000000", + }, + }, +}; + +export const ETH_OsmosisToEthereum_Route = { + route: { + estimate: { + fromAmount: "1000000000000000000000", + sendAmount: "1000000000000000000000", + toAmount: "999995820694001025771", + toAmountMin: "999995820694001025771", + fromAmountUSD: "3,496,020.00", + route: { + fromChain: [ + { + type: "Transfer", + fromChain: "osmosis-1", + toChain: "axelar-dojo-1", + fromToken: { + chainId: "osmosis-1", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + name: "Axelar ETH", + symbol: "axlETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + ibcDenom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + pathKey: "axleth_osmosis", + }, + toToken: { + chainId: "axelar-dojo-1", + address: "weth-wei", + name: "Axelar ETH", + symbol: "axlWETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + bridgeOnly: true, + ibcDenom: "weth-wei", + pathKey: "axleth_axelar", + }, + fromChannel: "channel-208", + toChannel: "channel-3", + }, + ], + toChain: [ + { + type: "CUSTOM", + callType: 1, + target: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + value: "0", + callData: + "0xa9059cbb0000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b90000000000000000000000000000000000000000000000000000000000000000", + payload: { + tokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + inputPos: 1, + }, + estimatedGas: "70000", + }, + ], + }, + feeCosts: [ + { + name: "Gas Receiver Fee", + description: "Estimated Gas Receiver fee", + percentage: "0", + token: { + chainId: "osmosis-1", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + name: "Axelar ETH", + symbol: "axlETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + ibcDenom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + pathKey: "axleth_osmosis", + }, + amount: "4179305998974229", + amountUSD: "14.6109", + }, + ], + gasCosts: [ + { + type: "executeCall", + token: { + chainId: "osmosis-1", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + name: "Axelar ETH", + symbol: "axlETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + ibcDenom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + pathKey: "axleth_osmosis", + }, + amount: "20000", + amountUSD: "0.0000", + gasPrice: "0.25uosmo", + maxFeePerGas: "1.5", + estimate: "500000000000", + limit: "650000000000", + }, + ], + estimatedRouteDuration: 60, + isExpressSupported: false, + exchangeRate: "1.0", + aggregatePriceImpact: "0.0", + toAmountUSD: "3,496,005.3891", + toAmountMinUSD: "3,496,005.3891", + }, + params: { + collectFees: { + feeLocation: "NONE", + }, + receiveGasOnDestination: false, + enableExpress: false, + slippage: 1, + quoteOnly: false, + toAddress: "0x6c515B41bFBEe0aA754F306098Ba005152c928b9", + fromAddress: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + fromAmount: "1000000000000000000000", + toToken: { + chainId: 1, + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + name: "Wrapped ETH", + symbol: "WETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + }, + fromToken: { + chainId: "osmosis-1", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + name: "Axelar ETH", + symbol: "axlETH", + decimals: 18, + logoURI: + "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + coingeckoId: "weth", + commonKey: "weth-wei", + ibcDenom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + pathKey: "axleth_osmosis", + }, + toChain: "1", + fromChain: "osmosis-1", + }, + transactionRequest: { + routeType: "CALL_BRIDGE_CALL", + data: '{"msgTypeUrl":"/ibc.applications.transfer.v1.MsgTransfer","msg":{"sourcePort":"transfer","sourceChannel":"channel-208","token":{"denom":"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5","amount":"1000000000000000000000"},"sender":"osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx","receiver":"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5","timeoutTimestamp":{"low":-2012515328,"high":400233074,"unsigned":false},"memo":"{\\"destination_chain\\":\\"ethereum\\",\\"destination_address\\":\\"0xce16F69375520ab01377ce7B88f5BA8C48F8D666\\",\\"payload\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,108,81,91,65,191,190,224,170,117,79,48,96,152,186,0,81,82,201,40,185,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68,169,5,156,187,0,0,0,0,0,0,0,0,0,0,0,0,108,81,91,65,191,190,224,170,117,79,48,96,152,186,0,81,82,201,40,185,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\\"type\\":2,\\"fee\\":{\\"amount\\":\\"4179305998974229\\",\\"recipient\\":\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\"}}"}}', + value: "4179305998974229", + gasLimit: "650000000000", + gasPrice: "0.25uosmo", + maxFeePerGas: "1.5", + }, + }, +}; diff --git a/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts b/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts index c39464f1c7..d84d20a96c 100644 --- a/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts +++ b/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts @@ -9,7 +9,12 @@ import { server } from "../../__tests__/msw"; import { BridgeQuoteError } from "../../errors"; import { BridgeProviderContext } from "../../interface"; import { SquidBridgeProvider } from "../index"; -import { MockChains, MockTokens } from "./mocks"; +import { + ETH_OsmosisToEthereum_Route, + ETHtoAVAX_EthereumToAvalanche_Route, + MockChains, + MockTokens, +} from "./mocks"; jest.mock("viem", () => ({ ...jest.requireActual("viem"), @@ -62,20 +67,20 @@ beforeEach(() => { }) ); }), - rest.get("https://api.0xsquid.com/v1/tokens", (_req, res, ctx) => { - return res( + rest.get("https://api.0xsquid.com/v1/tokens", (_req, res, ctx) => + res( ctx.json({ tokens: MockTokens, }) - ); - }), - rest.get("https://api.0xsquid.com/v1/chains", (_req, res, ctx) => { - return res( + ) + ), + rest.get("https://api.0xsquid.com/v1/chains", (_req, res, ctx) => + res( ctx.json({ chains: MockChains, }) - ); - }) + ) + ) ); }); @@ -104,7 +109,13 @@ describe("SquidBridgeProvider", () => { provider = new SquidBridgeProvider("integratorId", ctx); }); - it("should get a quote", async () => { + it("should get a quote - ETH from Ethereum to AVAX on Avalanche", async () => { + server.use( + rest.get("https://api.0xsquid.com/v1/route", (_req, res, ctx) => + res(ctx.json(ETHtoAVAX_EthereumToAvalanche_Route)) + ) + ); + const quote = await provider.getQuote({ fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, @@ -112,26 +123,29 @@ describe("SquidBridgeProvider", () => { denom: "ETH", address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", decimals: 18, - sourceDenom: "avax", }, - fromAmount: "1", - fromAddress: "0x742d35Cc6634C0532925a3b", + fromAmount: "1000000000000000", + fromAddress: "0x7863Ec05b123885c7609B05c35Df777F3F180258", toAddress: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", slippage: 1, }); expect(quote).toBeDefined(); expect(quote).toEqual({ - input: { amount: "1", sourceDenom: "eth", decimals: 18, denom: "ETH" }, + input: { + amount: "1000000000000000", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + denom: "ETH", + }, expectedOutput: { - amount: "0.99", - sourceDenom: "avax", + amount: "54602787339179287", + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", decimals: 18, denom: "AVAX", priceImpact: "0.000000000000000000", @@ -139,25 +153,126 @@ describe("SquidBridgeProvider", () => { fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, toChain: { chainId: 43114, chainName: "Avalanche", chainType: "evm" }, transferFee: { - amount: "0.01", + amount: "551234033843310", denom: "ETH", - sourceDenom: "ETH", + chainId: 1, + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, }, - estimatedTime: 900, + estimatedTime: 960, estimatedGasFee: { - amount: "0.00042", + amount: "6259874503623000", denom: "ETH", - sourceDenom: "ETH", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, }, transactionRequest: { type: "evm", - to: "0x0000000000000000000000000000000000000000", - data: "0xa9059cbb0000000000000000000000001234567890abcdef1234567890abcdef123456780000000000000000000000000000000000000000000000000000000000000001", - gas: "0x5208", - gasPrice: "0x3b9aca00", - approvalTransactionRequest: undefined, + to: "0xce16F69375520ab01377ce7B88f5BA8C48F8D666", + data: "0x846a1bc6000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000054000000000000000000000000000000000000000000000000000000000000005a00000000000000000000000007863ec05b123885c7609b05c35df777f3f18025800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000001b81d678ffb9c0263b24a97847620c99d213eb1400000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b81d678ffb9c0263b24a97847620c99d213eb14000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000104414bf389000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000001903b771a8400000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000003561ab00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004555344430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000094176616c616e6368650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30786365313646363933373535323061623031333737636537423838663542413843343846384436363600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164000000000000000000000000000000000000000000000000000000000000000400000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b900000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000082000000000000000000000000000000000000000000000000000000000000009a00000000000000000000000000000000000000000000000000000000000000be00000000000000000000000000000000000000000000000000000000000000d60000000000000000000000000000000000000000000000000000000000000122000000000000000000000000000000000000000000000000000000000000013a000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d9901230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d990123000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000003840651cb35000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000d7bb79aee866672419999a0496d99c54741d67b5000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e848000000000000000000000000000000000000000000000000000000000001e65f20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000104676528d100000000000000000000000000000000000000000000000000000000001e854e0000000000000000000000000000000000000000000000000100285a98347f5500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b9000000000000000000000000000000000000000000000000000001903b771a880000000000000000000000000000000000000000000000000000000000000002000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000b31f66aa3c1e785363f0875a1b74e27b85fd66c7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d9901230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bff334f8d5912ac5c4f2c590a2396d1c5d990123000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000003840651cb35000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec000000000000000000000000d7bb79aee866672419999a0496d99c54741d67b5000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016f8bd000000000000000000000000000000000000000000000000000000000016ca230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000fab550568c688d5d8a52c7d794cb93edc26ec0ec00000000000000000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000060ae616a2155ee3d9a68541ba4544862310933d4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000010438ed1739000000000000000000000000000000000000000000000000000000000016f95800000000000000000000000000000000000000000000000000c00c5619aac06600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000006c515b41bfbee0aa754f306098ba005152c928b9000000000000000000000000000000000000000000000000000001903b771a8c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e000000000000000000000000b31f66aa3c1e785363f0875a1b74e27b85fd66c7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000", + gas: "0xa8368", + maxFeePerGas: "0x3e18b346e", + maxPriorityFeePerGas: "0x59682f00", + value: "0x1f5582cc67c6e", + approvalTransactionRequest: { + // from encodeFunctionData + data: "0xabcdef", + to: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + }, + }, + }); + }); + + it("should get a quote - ETH from Osmosis to Ethereum", async () => { + server.use( + rest.get("https://api.0xsquid.com/v1/route", (_req, res, ctx) => + res(ctx.json(ETH_OsmosisToEthereum_Route)) + ) + ); + + const quote = await provider.getQuote({ + fromChain: { + chainId: "osmosis-1", + chainName: "Osmosis", + chainType: "cosmos", + }, + toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, + fromAsset: { + denom: "ETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + }, + toAsset: { + denom: "WETH", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + }, + fromAmount: "1000000000000000000000", + fromAddress: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + toAddress: "0x6c515B41bFBEe0aA754F306098Ba005152c928b9", + slippage: 1, + }); + + expect(quote).toBeDefined(); + expect(quote).toEqual({ + input: { + amount: "1000000000000000000000", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + denom: "ETH", + }, + expectedOutput: { + amount: "999995820694001025771", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + denom: "WETH", + priceImpact: "0.000000000000000000", + }, + fromChain: { + chainId: "osmosis-1", + chainName: "Osmosis", + chainType: "cosmos", + }, + toChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, + transferFee: { + amount: "4179305998974229", + denom: "axlETH", + chainId: "osmosis-1", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + }, + estimatedTime: 60, + estimatedGasFee: { + amount: "20000", + denom: "axlETH", + address: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + decimals: 18, + }, + transactionRequest: { + type: "cosmos", + msgTypeUrl: "/ibc.applications.transfer.v1.MsgTransfer", + msg: { + memo: '{"destination_chain":"ethereum","destination_address":"0xce16F69375520ab01377ce7B88f5BA8C48F8D666","payload":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,108,81,91,65,191,190,224,170,117,79,48,96,152,186,0,81,82,201,40,185,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,68,169,5,156,187,0,0,0,0,0,0,0,0,0,0,0,0,108,81,91,65,191,190,224,170,117,79,48,96,152,186,0,81,82,201,40,185,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,192,42,170,57,178,35,254,141,10,14,92,79,39,234,217,8,60,117,108,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"type":2,"fee":{"amount":"4179305998974229","recipient":"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89"}}', + receiver: + "axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5", + sender: "osmo107vyuer6wzfe7nrrsujppa0pvx35fvplp4t7tx", + sourceChannel: "channel-208", + sourcePort: "transfer", + timeoutHeight: { + revisionHeight: "1000", + revisionNumber: "1", + }, + timeoutTimestamp: "1718987965889999872", + token: { + amount: "1000000000000000000000", + denom: + "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + }, + }, }, }); }); @@ -179,13 +294,11 @@ describe("SquidBridgeProvider", () => { denom: "ETH", address: "0x0", decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0x0", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x123", @@ -201,7 +314,6 @@ describe("SquidBridgeProvider", () => { denom: "ETH", address: "0x0000000000000000000000000000000000000000", decimals: 18, - sourceDenom: "eth", }, fromChain: { chainId: 1, chainName: "Ethereum", chainType: "evm" }, fromAddress: "0x1234567890abcdef1234567890abcdef12345678", @@ -2683,13 +2795,11 @@ describe("SquidBridgeProvider", () => { denom: "ETH", address: "0x0", decimals: 18, - sourceDenom: "eth", }, toAsset: { denom: "AVAX", address: "0x0", decimals: 18, - sourceDenom: "avax", }, fromAmount: "1", fromAddress: "0x123", @@ -2773,7 +2883,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -2784,7 +2893,6 @@ describe("SquidBridgeProvider", () => { address: "uusdc", denom: "USDC", decimals: 6, - sourceDenom: "uusdc", }, { chainId: 1, @@ -2792,7 +2900,6 @@ describe("SquidBridgeProvider", () => { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", denom: "USDC", decimals: 6, - sourceDenom: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", }, ]); }); @@ -2808,7 +2915,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", decimals: 6, - sourceDenom: "uusdc", }, }); @@ -2820,7 +2926,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "uusdc", decimals: 6, - sourceDenom: "uusdc", }, { chainId: 1, @@ -2829,7 +2934,6 @@ describe("SquidBridgeProvider", () => { denom: "USDC", address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6, - sourceDenom: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", }, { chainId: "agoric-3", @@ -2839,8 +2943,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/295548A78785A1007F232DE286149A6FF512F180AF5657780FC89C009E2C348F", decimals: 6, - sourceDenom: - "ibc/295548A78785A1007F232DE286149A6FF512F180AF5657780FC89C009E2C348F", }, { chainId: 42161, @@ -2849,7 +2951,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "archway-1", @@ -2859,8 +2960,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/B9E4FD154C92D3A23BEA029906C4C5FF2FE74CB7E3A058290B77197A263CF88B", decimals: 6, - sourceDenom: - "ibc/B9E4FD154C92D3A23BEA029906C4C5FF2FE74CB7E3A058290B77197A263CF88B", }, { chainId: "mantle-1", @@ -2870,8 +2969,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/616E26A85AD20A3DDEAEBDDE7262E3BA9356C557BC15CACEA86768D7D51FA703", decimals: 6, - sourceDenom: - "ibc/616E26A85AD20A3DDEAEBDDE7262E3BA9356C557BC15CACEA86768D7D51FA703", }, { chainId: 43114, @@ -2880,7 +2977,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", decimals: 6, - sourceDenom: "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", }, { chainId: 8453, @@ -2889,7 +2985,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 56, @@ -2898,7 +2993,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0x4268B8F0B87b6Eae5d897996E6b845ddbD99Adf3", decimals: 6, - sourceDenom: "0x4268B8F0B87b6Eae5d897996E6b845ddbD99Adf3", }, { chainId: 81457, @@ -2907,7 +3001,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "carbon-1", @@ -2917,8 +3010,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/7C0807A56073C4A27B0DE1C21BA3EB75DF75FD763F4AD37BC159917FC01145F0", decimals: 6, - sourceDenom: - "ibc/7C0807A56073C4A27B0DE1C21BA3EB75DF75FD763F4AD37BC159917FC01145F0", }, { chainId: 42220, @@ -2927,7 +3018,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "comdex-1", @@ -2937,8 +3027,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/E1616E7C19EA474C565737709A628D6F8A23FF9D3E9A7A6871306CF5E0A5341E", decimals: 6, - sourceDenom: - "ibc/E1616E7C19EA474C565737709A628D6F8A23FF9D3E9A7A6871306CF5E0A5341E", }, { chainId: "crescent-1", @@ -2948,8 +3036,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/BFF0D3805B50D93E2FA5C0B2DDF7E0B30A631076CD80BC12A48C0E95404B4A41", decimals: 6, - sourceDenom: - "ibc/BFF0D3805B50D93E2FA5C0B2DDF7E0B30A631076CD80BC12A48C0E95404B4A41", }, { chainId: "dymension_1100-1", @@ -2959,8 +3045,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/BFAAB7870A9AAABF64A7366DAAA0B8E5065EAA1FCE762F45677DC24BE796EF65", decimals: 6, - sourceDenom: - "ibc/BFAAB7870A9AAABF64A7366DAAA0B8E5065EAA1FCE762F45677DC24BE796EF65", }, { chainId: "evmos_9001-2", @@ -2969,7 +3053,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "uusdc", decimals: 6, - sourceDenom: "uusdc", }, { chainId: 250, @@ -2978,7 +3061,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0x1B6382DBDEa11d97f24495C9A90b7c88469134a4", decimals: 6, - sourceDenom: "0x1B6382DBDEa11d97f24495C9A90b7c88469134a4", }, { chainId: 314, @@ -2987,7 +3069,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 252, @@ -2996,7 +3077,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 13371, @@ -3005,7 +3085,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "injective-1", @@ -3015,8 +3094,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/7E1AF94AD246BE522892751046F0C959B768642E5671CC3742264068D49553C0", decimals: 6, - sourceDenom: - "ibc/7E1AF94AD246BE522892751046F0C959B768642E5671CC3742264068D49553C0", }, { chainId: "juno-1", @@ -3026,8 +3103,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/EAC38D55372F38F1AFD68DF7FE9EF762DCF69F26520643CF3F9D292A738D8034", decimals: 6, - sourceDenom: - "ibc/EAC38D55372F38F1AFD68DF7FE9EF762DCF69F26520643CF3F9D292A738D8034", }, { chainId: 2222, @@ -3036,7 +3111,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "kaiyo-1", @@ -3046,8 +3120,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/295548A78785A1007F232DE286149A6FF512F180AF5657780FC89C009E2C348F", decimals: 6, - sourceDenom: - "ibc/295548A78785A1007F232DE286149A6FF512F180AF5657780FC89C009E2C348F", }, { chainId: 59144, @@ -3056,7 +3128,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 5000, @@ -3065,7 +3136,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 1284, @@ -3074,7 +3144,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xCa01a1D0993565291051daFF390892518ACfAD3A", decimals: 6, - sourceDenom: "0xCa01a1D0993565291051daFF390892518ACfAD3A", }, { chainId: "neutron-1", @@ -3084,8 +3153,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", decimals: 6, - sourceDenom: - "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", }, { chainId: 10, @@ -3094,7 +3161,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: 137, @@ -3103,7 +3169,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed", decimals: 6, - sourceDenom: "0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed", }, { chainId: "regen-1", @@ -3113,8 +3178,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/334740505537E9894A64E8561030695016481830D7B36E6A9B6D13C608B55653", decimals: 6, - sourceDenom: - "ibc/334740505537E9894A64E8561030695016481830D7B36E6A9B6D13C608B55653", }, { chainId: 534352, @@ -3123,7 +3186,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", decimals: 6, - sourceDenom: "0xEB466342C4d449BC9f53A865D5Cb90586f405215", }, { chainId: "secret-4", @@ -3132,7 +3194,6 @@ describe("SquidBridgeProvider", () => { denom: "axlUSDC", address: "secret1vkq022x4q8t8kx9de3r84u669l65xnwf2lg3e6", decimals: 6, - sourceDenom: "secret1vkq022x4q8t8kx9de3r84u669l65xnwf2lg3e6", }, { chainId: "stargaze-1", @@ -3142,8 +3203,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/96274e25174ee93314d8b5636d2d2f70963e207c22f643ec41949a3cbeda4c72", decimals: 6, - sourceDenom: - "ibc/96274e25174ee93314d8b5636d2d2f70963e207c22f643ec41949a3cbeda4c72", }, { chainId: "columbus-5", @@ -3153,8 +3212,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/E1E3674A0E4E1EF9C69646F9AF8D9497173821826074622D831BAB73CCB99A2D", decimals: 6, - sourceDenom: - "ibc/E1E3674A0E4E1EF9C69646F9AF8D9497173821826074622D831BAB73CCB99A2D", }, { chainId: "phoenix-1", @@ -3164,8 +3221,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4", decimals: 6, - sourceDenom: - "ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4", }, { chainId: "umee-1", @@ -3175,8 +3230,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/49788C29CD84E08D25CA7BE960BC1F61E88FEFC6333F58557D236D693398466A", decimals: 6, - sourceDenom: - "ibc/49788C29CD84E08D25CA7BE960BC1F61E88FEFC6333F58557D236D693398466A", }, ]); }); @@ -3192,7 +3245,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 6, - sourceDenom: "weth-wei", }, }); @@ -3204,7 +3256,6 @@ describe("SquidBridgeProvider", () => { denom: "axlWETH", address: "weth-wei", decimals: 18, - sourceDenom: "weth-wei", }, { chainId: 1, @@ -3213,7 +3264,6 @@ describe("SquidBridgeProvider", () => { denom: "WETH", address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, - sourceDenom: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, { chainId: "agoric-3", @@ -3223,8 +3273,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/1B38805B1C75352B28169284F96DF56BDEBD9E8FAC005BDCC8CF0378C82AA8E7", decimals: 18, - sourceDenom: - "ibc/1B38805B1C75352B28169284F96DF56BDEBD9E8FAC005BDCC8CF0378C82AA8E7", }, { chainId: 42161, @@ -3233,7 +3281,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "archway-1", @@ -3243,8 +3290,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/13C5990F84FA5D472E1F8BB1BAAEA8774DA5F24128EC02B119107AD21FB52A61", decimals: 18, - sourceDenom: - "ibc/13C5990F84FA5D472E1F8BB1BAAEA8774DA5F24128EC02B119107AD21FB52A61", }, { chainId: "mantle-1", @@ -3254,8 +3299,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/3EFE89848528B4A5665D0102DB818C6B19E04E17455197E92BECC3C41A7F7D78", decimals: 18, - sourceDenom: - "ibc/3EFE89848528B4A5665D0102DB818C6B19E04E17455197E92BECC3C41A7F7D78", }, { chainId: 81457, @@ -3264,7 +3307,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: 42220, @@ -3273,7 +3315,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "comdex-1", @@ -3283,8 +3324,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/81C3A46287D7664A8FD19843AC8D0CFD6C284EF1F750C661C48B3544277B1B29", decimals: 18, - sourceDenom: - "ibc/81C3A46287D7664A8FD19843AC8D0CFD6C284EF1F750C661C48B3544277B1B29", }, { chainId: "crescent-1", @@ -3294,8 +3333,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/F1806958CA98757B91C3FA1573ECECD24F6FA3804F074A6977658914A49E65A3", decimals: 18, - sourceDenom: - "ibc/F1806958CA98757B91C3FA1573ECECD24F6FA3804F074A6977658914A49E65A3", }, { chainId: "dymension_1100-1", @@ -3305,8 +3342,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/E3AB0DFDE9E782262B770C32DF94AC2A92B93DC4825376D6F6C874D3C877864E", decimals: 18, - sourceDenom: - "ibc/E3AB0DFDE9E782262B770C32DF94AC2A92B93DC4825376D6F6C874D3C877864E", }, { chainId: 1, @@ -3315,7 +3350,6 @@ describe("SquidBridgeProvider", () => { denom: "ETH", address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, - sourceDenom: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", }, { chainId: "evmos_9001-2", @@ -3324,7 +3358,6 @@ describe("SquidBridgeProvider", () => { denom: "axlWETH", address: "weth-wei", decimals: 18, - sourceDenom: "weth-wei", }, { chainId: 250, @@ -3333,7 +3366,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xfe7eDa5F2c56160d406869A8aA4B2F365d544C7B", decimals: 18, - sourceDenom: "0xfe7eDa5F2c56160d406869A8aA4B2F365d544C7B", }, { chainId: 314, @@ -3342,7 +3374,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: 252, @@ -3351,7 +3382,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "injective-1", @@ -3361,8 +3391,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/65A6973F7A4013335AE5FFE623FE019A78A1FEEE9B8982985099978837D764A7", decimals: 18, - sourceDenom: - "ibc/65A6973F7A4013335AE5FFE623FE019A78A1FEEE9B8982985099978837D764A7", }, { chainId: "juno-1", @@ -3372,8 +3400,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/95A45A81521EAFDBEDAEEB6DA975C02E55B414C95AD3CE50709272366A90CA17", decimals: 18, - sourceDenom: - "ibc/95A45A81521EAFDBEDAEEB6DA975C02E55B414C95AD3CE50709272366A90CA17", }, { chainId: 2222, @@ -3382,7 +3408,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "kaiyo-1", @@ -3392,8 +3417,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/1B38805B1C75352B28169284F96DF56BDEBD9E8FAC005BDCC8CF0378C82AA8E7", decimals: 18, - sourceDenom: - "ibc/1B38805B1C75352B28169284F96DF56BDEBD9E8FAC005BDCC8CF0378C82AA8E7", }, { chainId: 59144, @@ -3402,7 +3425,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: 5000, @@ -3411,7 +3433,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "neutron-1", @@ -3421,8 +3442,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/A585C2D15DCD3B010849B453A2CFCB5E213208A5AB665691792684C26274304D", decimals: 18, - sourceDenom: - "ibc/A585C2D15DCD3B010849B453A2CFCB5E213208A5AB665691792684C26274304D", }, { chainId: "pirin-1", @@ -3432,8 +3451,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/A7C4A3FB19E88ABE60416125F9189DA680800F4CDD14E3C10C874E022BEFF04C", decimals: 18, - sourceDenom: - "ibc/A7C4A3FB19E88ABE60416125F9189DA680800F4CDD14E3C10C874E022BEFF04C", }, { chainId: "regen-1", @@ -3443,8 +3460,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/62B27C470C859CBCB57DC12FCBBD357DD44CAD673362B47503FAA77523ABA028", decimals: 18, - sourceDenom: - "ibc/62B27C470C859CBCB57DC12FCBBD357DD44CAD673362B47503FAA77523ABA028", }, { chainId: 534352, @@ -3453,7 +3468,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", decimals: 18, - sourceDenom: "0xb829b68f57CC546dA7E5806A929e53bE32a4625D", }, { chainId: "secret-4", @@ -3462,7 +3476,6 @@ describe("SquidBridgeProvider", () => { denom: "axlETH", address: "secret139qfh3nmuzfgwsx2npnmnjl4hrvj3xq5rmq8a0", decimals: 18, - sourceDenom: "secret139qfh3nmuzfgwsx2npnmnjl4hrvj3xq5rmq8a0", }, { chainId: "columbus-5", @@ -3472,8 +3485,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/9B68CC79EFF12D25AF712EB805C5062B8F97B2CCE5F3FE55B107EE03095514A3", decimals: 18, - sourceDenom: - "ibc/9B68CC79EFF12D25AF712EB805C5062B8F97B2CCE5F3FE55B107EE03095514A3", }, { chainId: "phoenix-1", @@ -3483,8 +3494,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/BC8A77AFBD872FDC32A348D3FB10CC09277C266CFE52081DE341C7EC6752E674", decimals: 18, - sourceDenom: - "ibc/BC8A77AFBD872FDC32A348D3FB10CC09277C266CFE52081DE341C7EC6752E674", }, { chainId: "umee-1", @@ -3494,8 +3503,6 @@ describe("SquidBridgeProvider", () => { address: "ibc/04CE51E6E02243E565AE676DD60336E48D455F8AAD0611FA0299A22FDAC448D6", decimals: 18, - sourceDenom: - "ibc/04CE51E6E02243E565AE676DD60336E48D455F8AAD0611FA0299A22FDAC448D6", }, ]); }); @@ -3533,14 +3540,12 @@ describe("SquidBridgeProvider.getExternalUrl", () => { address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", decimals: 18, denom: "ETH", - sourceDenom: "eth", }, toAsset: { address: "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", decimals: 18, denom: "ETH", - sourceDenom: "eth", }, toAddress: "destination-address", }); @@ -3559,14 +3564,12 @@ describe("SquidBridgeProvider.getExternalUrl", () => { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 18, denom: "ETH", - sourceDenom: "eth", }, toAsset: { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 18, denom: "ETH", - sourceDenom: "eth", }, toAddress: "destination-address", }); @@ -3585,14 +3588,12 @@ describe("SquidBridgeProvider.getExternalUrl", () => { address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", decimals: 18, denom: "USDC", - sourceDenom: "usdc", }, toAsset: { address: "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", decimals: 18, denom: "USDC", - sourceDenom: "usdc", }, toAddress: "destination-address", }); diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 058e4c1685..4caa6c9fe6 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -6,7 +6,7 @@ import { type TokensResponse, type TransactionRequest, } from "@0xsquid/sdk"; -import { CoinPretty, Dec } from "@keplr-wallet/unit"; +import { Dec } from "@keplr-wallet/unit"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; import { apiClient, ApiClientError, isNil } from "@osmosis-labs/utils"; import { cachified } from "cachified"; @@ -29,12 +29,12 @@ import { BridgeProvider, BridgeProviderContext, BridgeQuote, - BridgeSupportedAssetsParams, BridgeTransactionRequest, CosmosBridgeTransactionRequest, EvmBridgeTransactionRequest, GetBridgeExternalUrlParams, GetBridgeQuoteParams, + GetBridgeSupportedAssetsParams, } from "../interface"; import { cosmosMsgOpts } from "../msg"; import { BridgeAssetMap } from "../utils"; @@ -85,42 +85,26 @@ export class SquidBridgeProvider implements BridgeProvider { toChain, slippage, }), + ttl: process.env.NODE_ENV === "test" ? -1 : 20 * 1000, // 20 seconds getFreshValue: async (): Promise => { - const url = new URL(`${this.apiURL}/v1/route`); - - const amount = new CoinPretty( - { - coinDecimals: fromAsset.decimals, - coinDenom: fromAsset.denom, - coinMinimalDenom: fromAsset.sourceDenom ?? fromAsset.denom, - }, - fromAmount - ).toCoin().amount; - const getRouteParams: SquidGetRouteParams = { fromChain: fromChain.chainId.toString(), toChain: toChain.chainId.toString(), fromAddress, toAddress, - fromAmount: amount, + fromAmount, fromToken: fromAsset.address, toToken: toAsset.address, slippage, quoteOnly: false, + enableExpress: false, + receiveGasOnDestination: false, }; + const url = new URL(`${this.apiURL}/v1/route`); Object.entries(getRouteParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); - - if (fromChain.chainType === "cosmos") { - throw new BridgeQuoteError({ - errorType: "UnsupportedQuoteError", - message: - "Squid withdrawals are temporarily disabled. Please use the Axelar Bridge Provider instead.", - }); - } - const data = await apiClient(url.toString(), { headers: { "x-integrator-id": this.integratorId, @@ -140,6 +124,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (feeCosts.length > 1 || gasCosts.length > 1) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "Osmosis FrontEnd only supports a single fee and gas costs", @@ -148,6 +133,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (!data.route.transactionRequest) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "Squid failed to generate a transaction request for this quote", @@ -159,6 +145,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (!aggregatePriceImpact) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "Squid failed to generate a price impact for this quote", }); @@ -166,6 +153,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (data.route.params.toToken.address !== toAsset.address) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "toAsset mismatch", }); @@ -178,6 +166,7 @@ export class SquidBridgeProvider implements BridgeProvider { fromAmountUSD === "" ) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", message: "USD value not found", }); @@ -185,16 +174,12 @@ export class SquidBridgeProvider implements BridgeProvider { return { input: { + ...fromAsset, amount: estimateFromAmount, - sourceDenom: fromAsset.sourceDenom, - decimals: fromAsset.decimals, - denom: fromAsset.denom, }, expectedOutput: { + ...toAsset, amount: toAmount, - sourceDenom: toAsset.sourceDenom ?? toAsset.denom, - decimals: toAsset.decimals, - denom: toAsset.denom, priceImpact: new Dec(aggregatePriceImpact) .quo(new Dec(100)) .toString(), @@ -204,15 +189,16 @@ export class SquidBridgeProvider implements BridgeProvider { transferFee: { denom: feeCosts[0].token.symbol, amount: feeCosts[0].amount, + chainId: feeCosts[0].token.chainId, decimals: feeCosts[0].token.decimals, - sourceDenom: feeCosts[0].token.symbol, + address: feeCosts[0].token.address, }, estimatedTime: estimatedRouteDuration, estimatedGasFee: { denom: gasCosts[0].token.symbol, amount: gasCosts[0].amount, decimals: gasCosts[0].token.decimals, - sourceDenom: gasCosts[0].token.symbol, + address: gasCosts[0].token.address, }, transactionRequest: isEvmTransaction ? await this.createEvmTransaction({ @@ -225,14 +211,13 @@ export class SquidBridgeProvider implements BridgeProvider { : await this.createCosmosTransaction(transactionRequest.data), }; }, - ttl: 20 * 1000, // 20 seconds, }); } async getSupportedAssets({ chain, asset, - }: BridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { + }: GetBridgeSupportedAssetsParams): Promise<(BridgeChain & BridgeAsset)[]> { try { const [tokens, chains] = await Promise.all([ this.getTokens(), @@ -277,7 +262,6 @@ export class SquidBridgeProvider implements BridgeProvider { address: c.sourceDenom, denom: c.symbol, decimals: c.decimals, - sourceDenom: c.sourceDenom, }); } if (counterparty.chainType === "evm") { @@ -289,7 +273,6 @@ export class SquidBridgeProvider implements BridgeProvider { address: c.sourceDenom, denom: c.symbol, decimals: c.decimals, - sourceDenom: c.sourceDenom, }); } } @@ -330,7 +313,6 @@ export class SquidBridgeProvider implements BridgeProvider { denom: variant.symbol, address: variant.address, decimals: variant.decimals, - sourceDenom: variant.address, }); } @@ -367,6 +349,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (!squidFromChain) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "ApprovalTxError", message: "Error getting approval Tx", }); @@ -398,6 +381,7 @@ export class SquidBridgeProvider implements BridgeProvider { }); } catch (e) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "ApprovalTxError", message: `Error creating approval Tx: ${e}`, }); @@ -456,6 +440,7 @@ export class SquidBridgeProvider implements BridgeProvider { parsedData.msgTypeUrl !== "/ibc.applications.transfer.v1.MsgTransfer" ) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "CreateCosmosTxError", message: "Unknown message type. Osmosis FrontEnd only supports the transfer message type", @@ -494,6 +479,7 @@ export class SquidBridgeProvider implements BridgeProvider { if (error instanceof Error) { throw new BridgeQuoteError({ + bridgeId: SquidBridgeProvider.ID, errorType: "CreateCosmosTxError", message: error.message, }); @@ -513,7 +499,7 @@ export class SquidBridgeProvider implements BridgeProvider { return cachified({ cache: this.ctx.cache, key: SquidBridgeProvider.ID + "_chains", - ttl: 30 * 60 * 1000, // 30 minutes + ttl: process.env.NODE_ENV === "test" ? -1 : 30 * 60 * 1000, // 30 minutes getFreshValue: async () => { try { const data = await apiClient( @@ -532,7 +518,7 @@ export class SquidBridgeProvider implements BridgeProvider { return cachified({ cache: this.ctx.cache, key: SquidBridgeProvider.ID + "_tokens", - ttl: 30 * 60 * 1000, // 30 minutes + ttl: process.env.NODE_ENV === "test" ? -1 : 30 * 60 * 1000, // 30 minutes getFreshValue: async () => { try { const data = await apiClient( @@ -599,6 +585,7 @@ export class SquidBridgeProvider implements BridgeProvider { fromAsset, toAsset, }: GetBridgeExternalUrlParams): Promise { + // TODO get axelar ID for both assets const url = new URL( this.ctx.env === "mainnet" ? "https://app.squidrouter.com/" diff --git a/packages/server/src/queries/complex/assets/price/index.ts b/packages/server/src/queries/complex/assets/price/index.ts index c280dcd9bb..0cbc1dfe2e 100644 --- a/packages/server/src/queries/complex/assets/price/index.ts +++ b/packages/server/src/queries/complex/assets/price/index.ts @@ -34,6 +34,7 @@ export async function getAssetPrice({ asset: { coinDenom?: string } & ( | { coinMinimalDenom: string } | { sourceDenom: string } + | { chainId: number | string; address: string } ); currency?: CoingeckoVsCurrencies; priceProvider?: PriceProvider; @@ -41,6 +42,10 @@ export async function getAssetPrice({ const coinMinimalDenom = "coinMinimalDenom" in asset ? asset.coinMinimalDenom : undefined; const sourceDenom = "sourceDenom" in asset ? asset.sourceDenom : undefined; + const { chainId, address } = + "chainId" in asset && "address" in asset + ? asset + : { chainId: undefined, address: undefined }; const foundAsset = assetLists .map((assets) => assets.assets) @@ -48,7 +53,16 @@ export async function getAssetPrice({ .find( (asset) => (coinMinimalDenom && asset.coinMinimalDenom === coinMinimalDenom) || - (sourceDenom && asset.sourceDenom === sourceDenom) + (sourceDenom && asset.sourceDenom === sourceDenom) || + (chainId && + address && + asset.counterparty.some( + (counterparty) => + "chainId" in counterparty && + "address" in counterparty && + counterparty.chainId === chainId && + counterparty.address.toLowerCase() === address.toLowerCase() + )) ); if (!foundAsset) diff --git a/packages/web/components/bridge/more-bridge-options.tsx b/packages/web/components/bridge/more-bridge-options.tsx index 54abbe5e17..b0ab70311d 100644 --- a/packages/web/components/bridge/more-bridge-options.tsx +++ b/packages/web/components/bridge/more-bridge-options.tsx @@ -36,13 +36,11 @@ export const MoreBridgeOptions = observer( address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", decimals: asset!.coinDecimals, denom: asset!.coinDenom, - sourceDenom: asset!.sourceDenom, }, toAsset: { address: asset!.coinMinimalDenom, decimals: asset!.coinDecimals, denom: asset!.coinDenom, - sourceDenom: asset!.sourceDenom, }, fromChain: { chainId: 43114, chainType: "evm" }, toChain: { diff --git a/packages/web/components/complex/transfer.tsx b/packages/web/components/complex/transfer.tsx index c7cfbba42e..a9e2b8ce00 100644 --- a/packages/web/components/complex/transfer.tsx +++ b/packages/web/components/complex/transfer.tsx @@ -172,14 +172,19 @@ export const Transfer = observer( toAddressToDisplay = to.address; } - const toAddressIcnsName = formatICNSName( - queriesExternalStore.queryICNSNames.getQueryContract(toAddressToDisplay) - ?.primaryName - ); - const fromAddressIcnsName = formatICNSName( - queriesExternalStore.queryICNSNames.getQueryContract(from.address) - ?.primaryName - ); + const toAddressIcnsName = !toAddressToDisplay.startsWith("Ox") + ? formatICNSName( + queriesExternalStore.queryICNSNames.getQueryContract( + toAddressToDisplay + )?.primaryName + ) + : undefined; + const fromAddressIcnsName = !from.address.startsWith("0x") + ? formatICNSName( + queriesExternalStore.queryICNSNames.getQueryContract(from.address) + ?.primaryName + ) + : undefined; const isSwitchWalletVisibleForTo = to.address.length > 0 && diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 3d1ec9fa91..5106e68dae 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -95,6 +95,7 @@ export const useFeatureFlags = () => { !isMobile && launchdarklyFlags.swapToolSimulateFee && // 1-Click trading is dependent on the swap tool simulate fee flag launchdarklyFlags.oneClickTrading, + newDepositWithdrawFlow: false, _isInitialized: isDevModeWithoutClientID ? true : isInitialized, _isClientIDPresent: !!process.env.NEXT_PUBLIC_LAUNCH_DARKLY_CLIENT_SIDE_ID, } as Record; diff --git a/packages/web/integrations/ethereum/hooks/use-erc20-balance.ts b/packages/web/integrations/ethereum/hooks/use-erc20-balance.ts index bc8d5ab94c..d9fd5af6d4 100644 --- a/packages/web/integrations/ethereum/hooks/use-erc20-balance.ts +++ b/packages/web/integrations/ethereum/hooks/use-erc20-balance.ts @@ -21,13 +21,14 @@ export function useErc20Balance( ethWallet.accountAddress ); }, + enabled: !!ethWallet && !!ethWallet.send && !!ethWallet.accountAddress, }); if (!erc20Balance) return; return new CoinPretty( { coinDecimals: erc20Balance.decimals, - coinMinimalDenom: erc20Balance.symbol, + coinMinimalDenom: erc20ContractAddress ?? erc20Balance.symbol, coinDenom: erc20Balance.symbol, }, erc20Balance.amount diff --git a/packages/web/integrations/trust-walletconnect/client.ts b/packages/web/integrations/trust-walletconnect/client.ts index 367b0d5f91..251f9e5a81 100644 --- a/packages/web/integrations/trust-walletconnect/client.ts +++ b/packages/web/integrations/trust-walletconnect/client.ts @@ -9,12 +9,11 @@ import { export class TrustClient extends WCClient { async signAmino( - chainId: string, - signer: string, - signDoc: StdSignDoc, - signOptions?: SignOptions + _chainId: string, + _signer: string, + _signDoc: StdSignDoc, + _signOptions?: SignOptions ): Promise { - console.log(chainId, signer, signDoc, signOptions); throw new Error("Trust doesn't support `signAmino` method."); } diff --git a/packages/web/modals/bridge-transfer-v2.tsx b/packages/web/modals/bridge-transfer-v2.tsx index cdbd41f899..b511dc6fb3 100644 --- a/packages/web/modals/bridge-transfer-v2.tsx +++ b/packages/web/modals/bridge-transfer-v2.tsx @@ -491,9 +491,6 @@ export const TransferContent: FunctionComponent< source: "account" as const, asset: { denom: assetToBridge.balance.currency.coinDenom, - sourceDenom: - originCurrency?.coinMinimalDenom ?? - assetToBridge.balance.currency?.coinMinimalDenom!, address: assetToBridge.balance.currency.coinMinimalDenom, // IBC address decimals: assetToBridge.balance.currency.coinDecimals, }, @@ -511,11 +508,6 @@ export const TransferContent: FunctionComponent< source: "counterpartyAccount" as const, asset: { denom: assetToBridge.balance.denom, - sourceDenom: - useNativeToken && isDeposit - ? sourceChainConfig?.nativeWrapEquivalent?.tokenMinDenom! // deposit uses native/gas token denom - : originCurrency?.coinMinimalDenom ?? - assetToBridge.balance.currency?.coinMinimalDenom!, address: useNativeToken ? NativeEVMTokenConstantAddress : sourceChainConfig?.erc20ContractAddress!, @@ -605,7 +597,7 @@ export const TransferContent: FunctionComponent< { coinDecimals: estimatedGasFee.decimals, coinDenom: estimatedGasFee.denom, - coinMinimalDenom: estimatedGasFee.sourceDenom, + coinMinimalDenom: estimatedGasFee.address, }, new Dec(estimatedGasFee.amount) ).maxDecimals(8) @@ -615,7 +607,7 @@ export const TransferContent: FunctionComponent< { coinDecimals: transferFee.decimals, coinDenom: transferFee.denom, - coinMinimalDenom: transferFee.sourceDenom, + coinMinimalDenom: transferFee.address, }, new Dec(transferFee.amount) ).maxDecimals(8), @@ -624,7 +616,7 @@ export const TransferContent: FunctionComponent< { coinDecimals: expectedOutput.decimals, coinDenom: expectedOutput.denom, - coinMinimalDenom: expectedOutput.sourceDenom, + coinMinimalDenom: expectedOutput.address, }, new Dec(expectedOutput.amount) ), diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 452c585da7..9054065234 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -78,31 +78,38 @@ export const bridgeTransferRouter = createTRPCRouter({ // Getting the fiat value from quotes here // results in more accurate fiat prices // and fair competition amongst bridge providers. + // Prices are on Osmosis, so a counterparty asset should match either the source denom in the asset list + // or the coinMinimalDenom on Osmosis. If not on Osmosis & no match, no price is provided. + // TODO: add coingeckoId to bridge provider assets to get price from coingecko in those cases. const [toAssetPrice, feeAssetPrice, gasFeeAssetPrice] = await Promise.all( [ getAssetPrice({ ...ctx, asset: { - coinDenom: input.toAsset.denom, - coinMinimalDenom: input.toAsset.denom, - sourceDenom: input.toAsset.sourceDenom, + coinMinimalDenom: input.toAsset.address, + sourceDenom: input.toAsset.address, + chainId: input.toChain.chainId, + address: input.toAsset.address, }, }), getAssetPrice({ ...ctx, asset: { - coinDenom: quote.transferFee.denom, - coinMinimalDenom: quote.transferFee.denom, - sourceDenom: quote.transferFee.sourceDenom, + coinMinimalDenom: quote.transferFee.address, + sourceDenom: quote.transferFee.address, + chainId: quote.transferFee.chainId, + address: quote.transferFee.address, }, }).catch(() => { // it's common for bridge providers to not provide correct denoms console.warn( "getQuoteByBridge: Failed to get asset price for transfer fee", { - coinDenom: quote.transferFee.denom, - coinMinimalDenom: quote.transferFee.denom, - sourceDenom: quote.transferFee.sourceDenom, + bridge: input.bridge, + coinMinimalDenom: quote.transferFee.address, + sourceDenom: quote.transferFee.address, + chainId: quote.transferFee.chainId, + address: quote.transferFee.address, } ); return undefined; @@ -111,9 +118,10 @@ export const bridgeTransferRouter = createTRPCRouter({ ? getAssetPrice({ ...ctx, asset: { - coinDenom: quote.estimatedGasFee.denom, - coinMinimalDenom: quote.estimatedGasFee.denom, - sourceDenom: quote.estimatedGasFee.sourceDenom, + coinMinimalDenom: quote.estimatedGasFee.address, + sourceDenom: quote.estimatedGasFee.address, + chainId: quote.fromChain.chainId, + address: quote.estimatedGasFee.address, }, }).catch(() => { // it's common for bridge providers to not provide correct denoms @@ -121,9 +129,11 @@ export const bridgeTransferRouter = createTRPCRouter({ console.warn( "getQuoteByBridge: Failed to get asset price for gas fee", { - coinDenom: quote.estimatedGasFee.denom, - coinMinimalDenom: quote.estimatedGasFee.denom, - sourceDenom: quote.estimatedGasFee.sourceDenom, + bridge: input.bridge, + coinMinimalDenom: quote.estimatedGasFee.address, + sourceDenom: quote.estimatedGasFee.address, + chainId: quote.fromChain.chainId, + address: quote.estimatedGasFee.address, } ); return undefined; From 98f97b4fd73d4a906a1b17e6169c808df9a18f62 Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:08:17 +0200 Subject: [PATCH 04/15] Added kava.USDT test (#3413) --- packages/web/e2e/tests/swap.stables.spec.ts | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/web/e2e/tests/swap.stables.spec.ts b/packages/web/e2e/tests/swap.stables.spec.ts index 409ed51a65..b168f46c77 100644 --- a/packages/web/e2e/tests/swap.stables.spec.ts +++ b/packages/web/e2e/tests/swap.stables.spec.ts @@ -21,6 +21,8 @@ test.describe("Test Swap Stables feature", () => { "ibc/8242AD24008032E457D2E12D46588FD39FB54FB29680C6C7663D296B383C37C4"; let allUSDT = "factory/osmo1em6xs47hd82806f5cxgyufguxrrc7l0aqx7nzzptjuqgswczk8csavdxek/alloyed/allUSDT"; + let kavaUSDT = + "ibc/4ABBEF4C8926DDDB320AE5188CFD63267ABBCEFC0583E4AE05D6E5AA2401DDAB"; test.beforeAll(async () => { console.log("Before test setup Wallet Extension."); @@ -116,4 +118,34 @@ test.describe("Test Swap Stables feature", () => { expect(swapPage.isTransactionSuccesful(10)); expect(swapPage.getTransactionUrl()).toBeTruthy(); }); + + test("User should be able to swap USDT.axl to kava.USDT", async () => { + await swapPage.goto(); + await swapPage.selectPair("USDT.axl", "kava.USDT"); + await swapPage.enterAmount("0.1"); + await swapPage.showSwapInfo(); + const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); + expect(msgContentAmount).toBeTruthy(); + expect(msgContentAmount).toContain("denom: " + USDTa); + expect(msgContentAmount).toContain("sender: " + walletId); + expect(msgContentAmount).toContain("token_out_denom: " + kavaUSDT); + expect(swapPage.isTransactionBroadcasted(10)); + expect(swapPage.isTransactionSuccesful(10)); + expect(swapPage.getTransactionUrl()).toBeTruthy(); + }); + + test("User should be able to swap kava.USDT to USDT.axl", async () => { + await swapPage.goto(); + await swapPage.selectPair("kava.USDT", "USDT.axl"); + await swapPage.enterAmount("0.1"); + await swapPage.showSwapInfo(); + const { msgContentAmount } = await swapPage.swapAndGetWalletMsg(context); + expect(msgContentAmount).toBeTruthy(); + expect(msgContentAmount).toContain("denom: " + kavaUSDT); + expect(msgContentAmount).toContain("sender: " + walletId); + expect(msgContentAmount).toContain("token_out_denom: " + USDTa); + expect(swapPage.isTransactionBroadcasted(10)); + expect(swapPage.isTransactionSuccesful(10)); + expect(swapPage.getTransactionUrl()).toBeTruthy(); + }); }); From 2b0df2dc90196f64c93848d8baab5bab86b2aac1 Mon Sep 17 00:00:00 2001 From: JohnnyWyles <97029546+JohnnyWyles@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:33:28 +0100 Subject: [PATCH 05/15] Push new Astroport mainnet code ID (#3370) Uploaded in https://daodao.zone/dao/osmosis/proposals/791 Confirmed migrated: https://celatone.osmosis.zone/osmosis-1/codes/842 Both testnet and mainnet will now be v1.0.3 --- packages/server/src/queries/complex/pools/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/queries/complex/pools/env.ts b/packages/server/src/queries/complex/pools/env.ts index 1d047f725f..47cfc68b4c 100644 --- a/packages/server/src/queries/complex/pools/env.ts +++ b/packages/server/src/queries/complex/pools/env.ts @@ -2,7 +2,7 @@ import { IS_TESTNET } from "../../../env"; /** Cosmwasm Code Ids confirmed to be transmuter pools in current env. */ const TransmuterPoolCodeIds = IS_TESTNET ? ["3084"] : ["148"]; -const AstroportPclPoolCodeIds = IS_TESTNET ? ["8611"] : ["773"]; +const AstroportPclPoolCodeIds = IS_TESTNET ? ["8611"] : ["842"]; const WhitewhalePoolCodeIds = IS_TESTNET ? ["?"] : ["503", "641"]; export function getCosmwasmPoolTypeFromCodeId( From d2ff39e7cbb939b9d97ead64e9d8830a033bdc9b Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Fri, 28 Jun 2024 13:55:23 -0400 Subject: [PATCH 06/15] (Wallet Addition) Display Crypto.com Wallet in App Browser (#3416) --- packages/web/config/wallet-registry.ts | 1 + .../wallet-select/use-selectable-wallets.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/web/config/wallet-registry.ts b/packages/web/config/wallet-registry.ts index 26b294f84c..316f5f4f59 100644 --- a/packages/web/config/wallet-registry.ts +++ b/packages/web/config/wallet-registry.ts @@ -261,6 +261,7 @@ export const CosmosWalletRegistry: CosmosRegistryWallet[] = [ import("@cosmos-kit/cdcwallet-extension").then( (m) => m.CdcwalletExtensionWallet ), + mobileDisabled: false, async supportsChain(chainId) { const cdcAvailableChains: MainnetChainIds[] = [ "cosmoshub-4", diff --git a/packages/web/modals/wallet-select/use-selectable-wallets.ts b/packages/web/modals/wallet-select/use-selectable-wallets.ts index e748669008..569cf5f0f5 100644 --- a/packages/web/modals/wallet-select/use-selectable-wallets.ts +++ b/packages/web/modals/wallet-select/use-selectable-wallets.ts @@ -70,6 +70,23 @@ export const useSelectableWallets = ({ const _window = window as Record; const mobileWebModeName = "mobile-web"; + /** + * If on mobile and `leap` is in `window`, it means that the user enters + * the frontend from Leap's app in app browser. So, there is no need + * to use wallet connect, as it resembles the extension's usage. + */ + if ( + _window?.cdc_wallet?.cosmos && + _window?.cdc_wallet?.cosmos.mode === mobileWebModeName + ) { + return array + .filter( + (wallet) => + wallet.name === AvailableCosmosWallets.CryptocomWallet + ) + .map((wallet) => ({ ...wallet, mobileDisabled: false })); + } + /** * If on mobile and `leap` is in `window`, it means that the user enters * the frontend from Leap's app in app browser. So, there is no need From f86b855a806d1f4c15a8c5f80e116960e214530e Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Fri, 28 Jun 2024 14:27:49 -0400 Subject: [PATCH 07/15] Improve transfer toasts (#3414) * move transfer history store to web package (and rename) * show toasts during transfer * fix translations * update toast, update translations --- packages/stores/src/index.ts | 1 - packages/web/components/alert/toast.tsx | 86 ++++++++---- .../web/components/table/transfer-history.tsx | 4 +- .../transactions/use-recent-transfers.ts | 4 +- packages/web/integrations/axelar/transfer.tsx | 6 +- packages/web/localizations/de.json | 9 +- packages/web/localizations/en.json | 9 +- packages/web/localizations/es.json | 9 +- packages/web/localizations/fa.json | 9 +- packages/web/localizations/fr.json | 9 +- packages/web/localizations/gu.json | 9 +- packages/web/localizations/hi.json | 9 +- packages/web/localizations/ja.json | 9 +- packages/web/localizations/ko.json | 9 +- packages/web/localizations/pl.json | 9 +- packages/web/localizations/pt-br.json | 9 +- packages/web/localizations/ro.json | 9 +- packages/web/localizations/ru.json | 9 +- packages/web/localizations/tr.json | 9 +- packages/web/localizations/zh-cn.json | 9 +- packages/web/localizations/zh-hk.json | 9 +- packages/web/localizations/zh-tw.json | 9 +- packages/web/modals/bridge-transfer-v2.tsx | 6 +- packages/web/stores/root.ts | 33 +++-- .../stores/transfer-history.ts} | 130 ++++++++++++++---- 25 files changed, 327 insertions(+), 96 deletions(-) rename packages/{stores/src/bridge-history/index.ts => web/stores/transfer-history.ts} (55%) diff --git a/packages/stores/src/index.ts b/packages/stores/src/index.ts index 93ecbbf606..66e1025b47 100644 --- a/packages/stores/src/index.ts +++ b/packages/stores/src/index.ts @@ -1,5 +1,4 @@ export * from "./account"; -export * from "./bridge-history"; export * from "./chain"; export * from "./currency-registrar"; export * from "./derived-data"; diff --git a/packages/web/components/alert/toast.tsx b/packages/web/components/alert/toast.tsx index 4fd1fd2943..addc754547 100644 --- a/packages/web/components/alert/toast.tsx +++ b/packages/web/components/alert/toast.tsx @@ -1,55 +1,69 @@ import Image from "next/image"; import React, { FunctionComponent } from "react"; -import { toast, ToastOptions } from "react-toastify"; +import { + Id, + toast, + ToastContent, + ToastOptions as ReactToastifyOptions, +} from "react-toastify"; import { Alert, ToastType } from "~/components/alert"; import { Icon } from "~/components/assets"; import { t } from "~/hooks"; +export type ToastOptions = Partial & { + updateToastId?: Id; +}; + export function displayToast( alert: Alert, type: ToastType, - toastOptions?: Partial + toastOptions?: ToastOptions ) { toastOptions = { - ...{ - position: "top-right", - autoClose: 7000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: false, - progress: undefined, - pauseOnFocusLoss: false, - closeButton: ({ closeToast }) => ( - - ), - }, + position: "top-right", + autoClose: type === ToastType.LOADING ? 2000 : 7000, + hideProgressBar: true, + closeOnClick: false, + pauseOnHover: true, + draggable: false, + progress: undefined, + pauseOnFocusLoss: false, + closeButton: ({ closeToast }) => ( + + ), ...(toastOptions ?? {}), }; + const showToast = toastOptions.updateToastId + ? (content: ToastContent, opts: ToastOptions) => + toast.update(toastOptions.updateToastId!, { + render: content, + ...opts, + }) + : toast; switch (type) { case ToastType.SUCCESS: - toast(, toastOptions); + showToast(, toastOptions); break; case ToastType.ERROR: - toast(, toastOptions); + showToast(, toastOptions); break; case ToastType.LOADING: - toast(, { ...toastOptions, autoClose: 2000 }); + showToast(, toastOptions); break; case ToastType.ONE_CLICK_TRADING: - toast(, toastOptions); + showToast(, toastOptions); break; } } @@ -58,6 +72,8 @@ const LoadingToast: FunctionComponent = ({ titleTranslationKey, captionTranslationKey, captionElement, + learnMoreUrl, + learnMoreUrlCaption, }) => (
@@ -78,6 +94,18 @@ const LoadingToast: FunctionComponent = ({ : t(...captionTranslationKey)}

)} + {learnMoreUrl && learnMoreUrlCaption && ( + + {t(learnMoreUrlCaption ?? "Learn more")} +
+ +
+
+ )}
); diff --git a/packages/web/components/table/transfer-history.tsx b/packages/web/components/table/transfer-history.tsx index 2d9ceb0df6..306221ed1e 100644 --- a/packages/web/components/table/transfer-history.tsx +++ b/packages/web/components/table/transfer-history.tsx @@ -34,7 +34,7 @@ export const TransferHistoryTable: FunctionComponent = observer( ({ className }) => { const { chainStore, - nonIbcBridgeHistoryStore, + transferHistoryStore, ibcTransferHistoryStore, accountStore, } = useStore(); @@ -42,7 +42,7 @@ export const TransferHistoryTable: FunctionComponent = observer( const { chainId } = chainStore.osmosis; const address = accountStore.getWallet(chainId)?.address ?? ""; - const histories: History[] = nonIbcBridgeHistoryStore + const histories: History[] = transferHistoryStore .getHistoriesByAccount(address) .map( ({ diff --git a/packages/web/components/transactions/use-recent-transfers.ts b/packages/web/components/transactions/use-recent-transfers.ts index 1f8dadf5d2..4ed9bdd0a4 100644 --- a/packages/web/components/transactions/use-recent-transfers.ts +++ b/packages/web/components/transactions/use-recent-transfers.ts @@ -26,14 +26,14 @@ const osmosisChainId = ChainList[0].chain_id; /** Gets recent (pending and recent) bridge transfers from history stores. Requires caller to wrap in `observer`. */ export function useRecentTransfers(address?: string): RecentTransfer[] { - const { ibcTransferHistoryStore, nonIbcBridgeHistoryStore } = useStore(); + const { ibcTransferHistoryStore, transferHistoryStore } = useStore(); if (!address) { return []; } // reconcile histories from IBC and non-IBC history stores - return nonIbcBridgeHistoryStore + return transferHistoryStore .getHistoriesByAccount(address) .map( ({ diff --git a/packages/web/integrations/axelar/transfer.tsx b/packages/web/integrations/axelar/transfer.tsx index 30efca3d7f..41602380dc 100644 --- a/packages/web/integrations/axelar/transfer.tsx +++ b/packages/web/integrations/axelar/transfer.tsx @@ -81,7 +81,7 @@ export const AxelarTransfer: FunctionComponent< accountStore, queriesStore, queriesExternalStore, - nonIbcBridgeHistoryStore, + transferHistoryStore, } = useStore(); const { t } = useTranslation(); @@ -263,7 +263,7 @@ export const AxelarTransfer: FunctionComponent< const trackTransferStatus = useCallback( (txHash: string) => { if (inputAmountRaw !== "") { - nonIbcBridgeHistoryStore.pushTxNow( + transferHistoryStore.pushTxNow( `axelar${txHash}`, new CoinPretty(originCurrency, inputAmount).trim(true).toString(), isWithdraw, @@ -272,7 +272,7 @@ export const AxelarTransfer: FunctionComponent< } }, [ - nonIbcBridgeHistoryStore, + transferHistoryStore, originCurrency, inputAmountRaw, inputAmount, diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 5afb2a8e77..58a06e4200 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Wählen Sie aus den folgenden alternativen Anbietern zum Abheben (auch „Brückenübertragen“) Ihres {asset} auf {chain} .", "depositWith": "Einzahlung mit", "withdrawWith": "Abheben mit" - } + }, + "pendingWithdraw": "Ausstehender Auszug", + "pendingDeposit": "Ausstehende Einzahlung", + "completedWithdraw": "Auszahlung abgeschlossen", + "completedDeposit": "Einzahlung abgeschlossen", + "failedWithdraw": "Auszahlung fehlgeschlagen", + "failedDeposit": "Einzahlung fehlgeschlagen", + "connectionError": "Verbindungsfehler" }, "unknownError": "Unbekannter Fehler", "viewExplorer": "Explorer anzeigen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index c045d92234..af2c7b1c49 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Choose from the following alternative providers to withdraw (aka “bridge”) your {asset} to {chain}.", "depositWith": "Deposit with", "withdrawWith": "Withdraw with" - } + }, + "pendingWithdraw": "Pending withdraw", + "pendingDeposit": "Pending deposit", + "completedWithdraw": "Withdraw completed", + "completedDeposit": "Deposit completed", + "failedWithdraw": "Withdraw failed", + "failedDeposit": "Deposit failed", + "connectionError": "Connection error" }, "unknownError": "Unknown error", "viewExplorer": "View explorer", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index fd97f73ece..e2b91cf386 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Elija entre los siguientes proveedores alternativos para retirar (también conocido como “puente”) su {asset} a {chain} .", "depositWith": "Depositar con", "withdrawWith": "Retirarse con" - } + }, + "pendingWithdraw": "A la espera de Retiro", + "pendingDeposit": "Deposito pendiente", + "completedWithdraw": "Retiro completado", + "completedDeposit": "Depósito completado", + "failedWithdraw": "Retiro fallido", + "failedDeposit": "El depósito falló", + "connectionError": "Error de conexión" }, "unknownError": "Error desconocido", "viewExplorer": "Ver Explorador", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 5fb244f3b4..51856c12f3 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "از میان ارائه‌دهندگان جایگزین زیر {asset} خود را به {chain} .", "depositWith": "سپرده گذاری با", "withdrawWith": "برداشت با" - } + }, + "pendingWithdraw": "در انتظار برداشت", + "pendingDeposit": "سپرده معلق", + "completedWithdraw": "برداشت کامل شد", + "completedDeposit": "واریز تکمیل شد", + "failedWithdraw": "برداشت ناموفق بود", + "failedDeposit": "سپرده گذاری انجام نشد", + "connectionError": "خطای اتصال" }, "unknownError": "خطای نا شناس", "viewExplorer": "مشاهده جزئیات تراکنش", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index fbeeecade7..18fad289c1 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Choisissez parmi les fournisseurs alternatifs suivants pour retirer (alias « pont ») votre {asset} vers {chain} .", "depositWith": "Dépôt avec", "withdrawWith": "Retirer avec" - } + }, + "pendingWithdraw": "En attente de retrait", + "pendingDeposit": "Dépôt en attente", + "completedWithdraw": "Retrait terminé", + "completedDeposit": "Dépôt complété", + "failedWithdraw": "Échec du retrait", + "failedDeposit": "Échec du dépôt", + "connectionError": "Erreur de connexion" }, "unknownError": "Erreur inconnue", "viewExplorer": "Voir dans l'exploreur", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index a5308192d4..419cbfc598 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "તમારા {asset} થી {chain} ઉપાડવા માટે નીચેના વૈકલ્પિક પ્રદાતાઓમાંથી પસંદ કરો (ઉર્ફે \"બ્રિજ\"). .", "depositWith": "સાથે જમા", "withdrawWith": "સાથે પાછી ખેંચો" - } + }, + "pendingWithdraw": "બાકી ઉપાડ", + "pendingDeposit": "બાકી થાપણ", + "completedWithdraw": "ઉપાડ પૂર્ણ", + "completedDeposit": "ડિપોઝિટ પૂર્ણ", + "failedWithdraw": "ઉપાડ નિષ્ફળ", + "failedDeposit": "ડિપોઝિટ નિષ્ફળ", + "connectionError": "કનેક્શન ભૂલ" }, "unknownError": "અજાણી ભૂલ", "viewExplorer": "સંશોધક જુઓ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 6e092a9717..51cacf89cb 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "अपने {asset} को {chain} वापस लेने (उर्फ “ब्रिज”) के लिए निम्नलिखित वैकल्पिक प्रदाताओं में से चुनें।", "depositWith": "जमा करें", "withdrawWith": "साथ वापस लें" - } + }, + "pendingWithdraw": "लंबित निकासी", + "pendingDeposit": "लंबित जमा", + "completedWithdraw": "निकासी पूर्ण हुई", + "completedDeposit": "जमा पूर्ण हुआ", + "failedWithdraw": "निकासी विफल", + "failedDeposit": "जमा विफल", + "connectionError": "संपर्क त्रुटि" }, "unknownError": "अज्ञात त्रुटि", "viewExplorer": "एक्सप्लोरर देखें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index d18b3e8512..15d8c725e0 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "次の代替プロバイダーから選択して{asset}を{chain} 。", "depositWith": "入金", "withdrawWith": "撤退する" - } + }, + "pendingWithdraw": "保留中の撤回", + "pendingDeposit": "保留中の入金", + "completedWithdraw": "引き出しが完了しました", + "completedDeposit": "入金完了", + "failedWithdraw": "引き出しに失敗しました", + "failedDeposit": "入金に失敗しました", + "connectionError": "接続エラー" }, "unknownError": "不明なエラー", "viewExplorer": "エクスプローラーを表示する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 89de3a7a2a..68741be0b5 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "귀하의 {asset} {chain} 으로 철회(일명 \"브리지\")하려면 다음 대체 제공자 중에서 선택하세요. .", "depositWith": "다음으로 입금", "withdrawWith": "다음으로 인출" - } + }, + "pendingWithdraw": "출금 대기 중", + "pendingDeposit": "입금 대기 중", + "completedWithdraw": "출금 완료", + "completedDeposit": "입금완료", + "failedWithdraw": "출금 실패", + "failedDeposit": "입금 실패", + "connectionError": "연결 오류" }, "unknownError": "알 수 없는 에러", "viewExplorer": "블록 익스플로러 보기", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 3535fb15bc..a9b7b4adad 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Wybierz spośród następujących alternatywnych dostawców, aby wycofać (inaczej „bridge”) swój {asset} do {chain} .", "depositWith": "Wpłać za pomocą", "withdrawWith": "Wycofaj się z" - } + }, + "pendingWithdraw": "Oczekuje na wycofanie", + "pendingDeposit": "Oczekujący depozyt", + "completedWithdraw": "Wypłata zakończona", + "completedDeposit": "Depozyt zrealizowany", + "failedWithdraw": "Wypłata nie powiodła się", + "failedDeposit": "Wpłata nie powiodła się", + "connectionError": "Błąd połączenia" }, "unknownError": "Nieznany błąd", "viewExplorer": "zobacz eksplorer", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 94b53749e8..15923601d2 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Escolha um dos seguintes provedores alternativos para retirar (também conhecido como “ponte”) seu {asset} para {chain} .", "depositWith": "Deposite com", "withdrawWith": "Retirar com" - } + }, + "pendingWithdraw": "Retirada pendente", + "pendingDeposit": "Depósito pendente", + "completedWithdraw": "Retirada concluída", + "completedDeposit": "Depósito concluído", + "failedWithdraw": "Falha na retirada", + "failedDeposit": "Falha no depósito", + "connectionError": "Erro de conexão" }, "unknownError": "Erro desconhecido", "viewExplorer": "Visualizar explorer", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index e82829f9e2..d645c0421d 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Alegeți dintre următorii furnizori alternativi pentru a vă retrage {asset} la {chain} .", "depositWith": "Depozit cu", "withdrawWith": "Retrage cu" - } + }, + "pendingWithdraw": "În așteptarea retragerii", + "pendingDeposit": "Depunerea în așteptare", + "completedWithdraw": "Retragere finalizată", + "completedDeposit": "Depozit finalizat", + "failedWithdraw": "Retragerea a eșuat", + "failedDeposit": "Depunerea nu a reușit", + "connectionError": "Eroare de conexiune" }, "unknownError": "Eroare necunoscuta", "viewExplorer": "vezi explorer", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 9a0f811961..71d39ead12 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "Выберите одного из следующих альтернативных поставщиков для вывода (так называемого «моста») вашего {asset} в {chain} .", "depositWith": "Депозит с", "withdrawWith": "Вывод средств с помощью" - } + }, + "pendingWithdraw": "Ожидание вывода", + "pendingDeposit": "Ожидается депозит", + "completedWithdraw": "Вывод завершен", + "completedDeposit": "Депозит завершен", + "failedWithdraw": "Вывести не удалось", + "failedDeposit": "Депозит не выполнен", + "connectionError": "Ошибка подключения" }, "unknownError": "Неизвестная ошибка", "viewExplorer": "Посмотреть проводник", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 634eb89014..5fe0bea91b 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "{asset} {chain} .", "depositWith": "Şununla para yatır:", "withdrawWith": "Şununla çekil:" - } + }, + "pendingWithdraw": "Geri çekilme bekleniyor", + "pendingDeposit": "Bekleyen mevduat", + "completedWithdraw": "Para çekme işlemi tamamlandı", + "completedDeposit": "Para yatırma tamamlandı", + "failedWithdraw": "Para çekme işlemi başarısız oldu", + "failedDeposit": "Para yatırma işlemi başarısız oldu", + "connectionError": "Bağlantı hatası" }, "unknownError": "Bilinmeyen hata", "viewExplorer": "gezginde görüntüle", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 7152ac61df..f97b5995d0 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "从以下备选提供商中进行选择,将您的{asset}提取(又称为“桥接”)到{chain} 。", "depositWith": "存款", "withdrawWith": "提款" - } + }, + "pendingWithdraw": "等待提款", + "pendingDeposit": "待存款", + "completedWithdraw": "提现完成", + "completedDeposit": "存款已完成", + "failedWithdraw": "提现失败", + "failedDeposit": "存款失败", + "connectionError": "连接错误" }, "unknownError": "未知错误", "viewExplorer": "浏览器查看", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 4470b69710..da80f75b56 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "從以下替代提供者中進行選擇,將您的{asset}撤回(也稱為「bridge」)到{chain} 。", "depositWith": "存款於", "withdrawWith": "撤回與" - } + }, + "pendingWithdraw": "待提款", + "pendingDeposit": "待定存款", + "completedWithdraw": "提現完成", + "completedDeposit": "存款完成", + "failedWithdraw": "提現失敗", + "failedDeposit": "存款失敗", + "connectionError": "連線錯誤" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index a2c4575340..364d01c7cf 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -929,7 +929,14 @@ "descriptionWithdraw": "從以下替代提供者中進行選擇,將您的{asset}撤回(也稱為「bridge」)到{chain} 。", "depositWith": "存款於", "withdrawWith": "撤回與" - } + }, + "pendingWithdraw": "待提款", + "pendingDeposit": "待定存款", + "completedWithdraw": "提現完成", + "completedDeposit": "存款完成", + "failedWithdraw": "提現失敗", + "failedDeposit": "存款失敗", + "connectionError": "連線錯誤" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/modals/bridge-transfer-v2.tsx b/packages/web/modals/bridge-transfer-v2.tsx index b511dc6fb3..cf0757bed9 100644 --- a/packages/web/modals/bridge-transfer-v2.tsx +++ b/packages/web/modals/bridge-transfer-v2.tsx @@ -371,7 +371,7 @@ export const TransferContent: FunctionComponent< chainStore, accountStore, queriesStore, - nonIbcBridgeHistoryStore, + transferHistoryStore, } = useStore(); const { showModalBase, @@ -772,7 +772,7 @@ export const TransferContent: FunctionComponent< const trackTransferStatus = useCallback( (providerId: Bridge, params: GetTransferStatusParams) => { if (inputAmountRaw !== "") { - nonIbcBridgeHistoryStore.pushTxNow( + transferHistoryStore.pushTxNow( `${providerId}${JSON.stringify(params)}`, new CoinPretty(originCurrency, inputAmount).trim(true).toString(), isWithdraw, @@ -782,7 +782,7 @@ export const TransferContent: FunctionComponent< }, [ inputAmountRaw, - nonIbcBridgeHistoryStore, + transferHistoryStore, originCurrency, inputAmount, isWithdraw, diff --git a/packages/web/stores/root.ts b/packages/web/stores/root.ts index 826bf331db..c75e1b113a 100644 --- a/packages/web/stores/root.ts +++ b/packages/web/stores/root.ts @@ -17,7 +17,6 @@ import { DerivedDataStore, IBCTransferHistoryStore, LPCurrencyRegistrar, - NonIbcBridgeHistoryStore, OsmosisAccount, OsmosisQueries, PoolFallbackPriceStore, @@ -58,6 +57,8 @@ import { UserSettings, } from "~/stores/user-settings"; +import { TransferHistoryStore } from "./transfer-history"; + const assets = AssetLists.flatMap((list) => list.assets); export class RootStore { @@ -78,7 +79,7 @@ export class RootStore { public readonly derivedDataStore: DerivedDataStore; public readonly ibcTransferHistoryStore: IBCTransferHistoryStore; - public readonly nonIbcBridgeHistoryStore: NonIbcBridgeHistoryStore; + public readonly transferHistoryStore: TransferHistoryStore; public readonly assetsStore: ObservableAssets; @@ -249,16 +250,26 @@ export class RootStore { makeIndexedKVStore("ibc_transfer_history"), this.chainStore ); - this.nonIbcBridgeHistoryStore = new NonIbcBridgeHistoryStore( - this.queriesStore, - this.chainStore.osmosis.chainId, + + const transferStatusProviders = [ + new AxelarTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), + new SquidTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), + new SkipTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), + new IbcTransferStatusProvider(ChainList, AssetLists), + ]; + + this.transferHistoryStore = new TransferHistoryStore( + (accountAddress) => { + this.queriesStore + .get(this.chainStore.osmosis.chainId) + .queryBalances.getQueryBech32Address(accountAddress) + .fetch(); + // txEvents passed to root store is used to invalidate + // tRPC queries, the params are not used + txEvents?.onFulfill?.("", ""); + }, makeLocalStorageKVStore("nonibc_transfer_history"), - [ - new AxelarTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), - new SquidTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), - new SkipTransferStatusProvider(IS_TESTNET ? "testnet" : "mainnet"), - new IbcTransferStatusProvider(ChainList, AssetLists), - ] + transferStatusProviders ); this.lpCurrencyRegistrar = new LPCurrencyRegistrar(this.chainStore); diff --git a/packages/stores/src/bridge-history/index.ts b/packages/web/stores/transfer-history.ts similarity index 55% rename from packages/stores/src/bridge-history/index.ts rename to packages/web/stores/transfer-history.ts index c035ae30ef..0bb5d3bbba 100644 --- a/packages/stores/src/bridge-history/index.ts +++ b/packages/web/stores/transfer-history.ts @@ -5,7 +5,6 @@ import { TransferStatusProvider, TransferStatusReceiver, } from "@osmosis-labs/bridge"; -import { CosmosQueries, IQueriesStore } from "@osmosis-labs/keplr-stores"; import { action, autorun, @@ -16,6 +15,8 @@ import { } from "mobx"; import { computedFn } from "mobx-utils"; +import { displayToast, ToastType } from "~/components/alert"; + /** Persistable data enough to identify a tx. */ type TxSnapshot = { /** From Date.getTime(). Assumed local timezone. */ @@ -30,25 +31,33 @@ type TxSnapshot = { const STORE_KEY = "nonibc_history_tx_snapshots"; -/** Stores and tracks status for non-IBC bridge transfers. - * Supports querying state from arbitrary remote chains via dependency injection. - * NOTE: source keyPrefix values must be unique. +/** + * Stores and tracks status for bridge transfers. + * NOTE: source keyPrefix values must be unique. */ -export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { +export class TransferHistoryStore implements TransferStatusReceiver { /** Volatile store of tx statuses. `prefixedKey => TxSnapshot` */ @observable protected snapshots: TxSnapshot[] = []; @observable private isRestoredFromLocalStorage = false; + /** + * Since we can't control how many times a status provider + * will call `receiveNewTxStatus`, we need to track which + * tx statuses have already been resolved to avoid duplicity in UI. + */ + private readonly _resolvedTxStatusKeys = new Set(); + constructor( - protected readonly queriesStore: IQueriesStore, - protected readonly chainId: string, + protected readonly onAccountTransferSuccess: ( + accountAddress: string + ) => void, protected readonly kvStore: KVStore, - protected readonly txStatusSources: TransferStatusProvider[] = [], + protected readonly transferStatusProviders: TransferStatusProvider[] = [], protected readonly historyExpireDays = 3 ) { - this.txStatusSources.forEach( + this.transferStatusProviders.forEach( (source) => (source.statusReceiverDelegate = this) ); @@ -64,10 +73,6 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { this.restoreSnapshots(); } - addStatusSource(source: TransferStatusProvider) { - this.txStatusSources.push(source); - } - getHistoriesByAccount = computedFn((accountAddress: string) => { const histories: { key: string; @@ -80,7 +85,7 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { isWithdraw: boolean; }[] = []; this.snapshots.forEach((snapshot) => { - const statusSource = this.txStatusSources.find((source) => + const statusSource = this.transferStatusProviders.find((source) => snapshot.prefixedKey.startsWith(source.keyPrefix) ); if (statusSource && snapshot.accountAddress === accountAddress) { @@ -116,7 +121,7 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { isWithdraw: boolean, accountAddress: string ) { - const statusSource = this.txStatusSources.find((source) => + const statusSource = this.transferStatusProviders.find((source) => prefixedKey.startsWith(source.keyPrefix) ); @@ -125,6 +130,19 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { prefixedKey.slice(statusSource.keyPrefix.length) ); + setTimeout(() => { + displayToast( + { + titleTranslationKey: isWithdraw + ? "transfer.pendingWithdraw" + : "transfer.pendingDeposit", + captionTranslationKey: amount, + }, + ToastType.LOADING, + { toastId: prefixedKey, autoClose: false } + ); + }, 500); + this.snapshots.push({ createdAtMs: Date.now(), prefixedKey, @@ -135,6 +153,10 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { }); } + /** + * Forward tx info the relevant status source to start tracking the transfer status + * of an initiated transfer. + */ @action receiveNewTxStatus( prefixedKey: string, @@ -150,16 +172,64 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { return; } - // update balances if successful - if (status === "success") { - this.queriesStore - .get(this.chainId) - .queryBalances.getQueryBech32Address(snapshot.accountAddress) - .fetch(); - } - + // set updates snapshot.status = status; snapshot.reason = reason; + + switch (status) { + case "pending": + displayToast( + { + titleTranslationKey: snapshot.isWithdraw + ? "transfer.pendingWithdraw" + : "transfer.pendingDeposit", + }, + ToastType.LOADING, + { updateToastId: prefixedKey, autoClose: false } + ); + break; + case "success": + if (this._resolvedTxStatusKeys.has(prefixedKey)) break; + displayToast( + { + titleTranslationKey: snapshot.isWithdraw + ? "transfer.completedWithdraw" + : "transfer.completedDeposit", + captionTranslationKey: snapshot.amount, + }, + ToastType.SUCCESS, + { updateToastId: prefixedKey } + ); + this.onAccountTransferSuccess(snapshot.accountAddress); + this._resolvedTxStatusKeys.add(prefixedKey); + break; + case "failed": + if (this._resolvedTxStatusKeys.has(prefixedKey)) break; + displayToast( + { + titleTranslationKey: snapshot.isWithdraw + ? "transfer.failedWithdraw" + : "transfer.failedDeposit", + captionTranslationKey: snapshot.amount, + }, + ToastType.ERROR, + { updateToastId: prefixedKey } + ); + this._resolvedTxStatusKeys.add(prefixedKey); + break; + case "connection-error": + if (this._resolvedTxStatusKeys.has(prefixedKey)) break; + displayToast( + { + titleTranslationKey: "transfer.connectionError", + captionTranslationKey: snapshot.amount, + }, + ToastType.ERROR, + { updateToastId: prefixedKey } + ); + this._resolvedTxStatusKeys.add(prefixedKey); + break; + } } /** Use persisted tx snapshots to resume Tx monitoring after browser first loads. @@ -173,14 +243,18 @@ export class NonIbcBridgeHistoryStore implements TransferStatusReceiver { if (this.isSnapshotExpired(snapshot)) { return; } - const statusSource = this.txStatusSources.find((source) => + const statusSource = this.transferStatusProviders.find((source) => snapshot.prefixedKey.startsWith(source.keyPrefix) ); - // start receiving tx status updates again - statusSource?.trackTxStatus( - snapshot.prefixedKey.slice(statusSource.keyPrefix.length) - ); + // start receiving tx status updates again for snapshots that were still pending + if (snapshot.status === "pending" && statusSource) { + statusSource.trackTxStatus( + snapshot.prefixedKey.slice(statusSource.keyPrefix.length) + ); + } else { + this._resolvedTxStatusKeys.add(snapshot.prefixedKey); + } runInAction(() => { this.snapshots.push(snapshot); From 774035bec7a37db6d18784f3f603c8b1d12f8489 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Fri, 28 Jun 2024 14:37:14 -0400 Subject: [PATCH 08/15] Use 1ct flag when estimating max balance gas (#3424) * use 1ct flag when estimating max balance gas * fix translation --- packages/web/.env | 2 +- packages/web/hooks/use-swap.tsx | 5 +++++ packages/web/stores/index.tsx | 4 +--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/web/.env b/packages/web/.env index 7182cddb4a..d1010d80d8 100644 --- a/packages/web/.env +++ b/packages/web/.env @@ -45,4 +45,4 @@ TWITTER_API_URL=https://api.twitter.com/ # Pools that are excluded from showing external boost incentives APRs. # NEXT_PUBLIC_EXCLUDED_EXTERNAL_BOOSTS_POOL_IDS= -# NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS= \ No newline at end of file +NEXT_PUBLIC_SPEND_LIMIT_CONTRACT_ADDRESS=osmo10xqv8rlpkflywm92k5wdmplzy7khtasl9c2c08psmvlu543k724sy94k74 \ No newline at end of file diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index fb90b5bd31..79b47e2d9e 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -827,6 +827,8 @@ function useSwapAmountInput({ const isQuoteForCurrentBalanceLoading = isQuoteForCurrentBalanceLoading_ && balanceQuoteQueryEnabled; + const { isOneClickTradingEnabled } = useOneClickTradingSession(); + const networkFeeQueryEnabled = !isQuoteForCurrentBalanceLoading && balanceQuoteQueryEnabled && @@ -839,6 +841,9 @@ function useSwapAmountInput({ chainId: chainStore.osmosis.chainId, messages: quoteForCurrentBalance?.messages, enabled: networkFeeQueryEnabled, + signOptions: { + useOneClickTrading: isOneClickTradingEnabled, + }, }); const isLoadingCurrentBalanceNetworkFee = networkFeeQueryEnabled && isLoadingCurrentBalanceNetworkFee_; diff --git a/packages/web/stores/index.tsx b/packages/web/stores/index.tsx index a227f5ddbe..97179977f9 100644 --- a/packages/web/stores/index.tsx +++ b/packages/web/stores/index.tsx @@ -49,9 +49,7 @@ export const StoreProvider = ({ children }: PropsWithChildren) => { onExceeds1CTNetworkFeeLimit: ({ finish, continueTx }) => { displayToast( { - titleTranslationKey: t( - "oneClickTrading.toast.networkFeeTooHigh" - ), + titleTranslationKey: "oneClickTrading.toast.networkFeeTooHigh", captionElement: (
-
- - - - -
- {[ - { - label: "USDC.e", - amount: `$80.00 ${t("transfer.available")}`, - active: true, - }, - { label: "USDC", amount: "$30.00", active: false }, - { label: "USDC.axl", amount: "$10.00", active: false }, - ].map(({ label, amount, active }, index) => ( - - ))} -
- -
- - {type === "deposit" - ? t("transfer.transferWith") - : t("transfer.transferTo")} - -
- {wallet?.walletInfo.prettyName} - {wallet?.walletInfo.prettyName} - -
-
- - - {({ open }) => ( -
- -
-
- - {t("transfer.receiveAsset")} - - -

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

-

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

-
- } - > - - -
- -
- USDC - -
-
- - - - - - - - - - - - )} -
- -
-
- - - {t("transfer.estimatingTime")} - -
- - - {t("transfer.calculatingFees")} - -
- -
- - - setIsMoreOptionsVisible(false)} - /> -
- - - ); -}); - -const ChainSelectorButton: FunctionComponent<{ - readonly?: boolean; - children: ReactNode; - chainLogo: string; -}> = ({ readonly, children, chainLogo: _chainLogo }) => { - const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); - - if (readonly) { - return ( -
- {children} -
- ); - } - - return ( - <> - - setIsNetworkSelectVisible(false)} - /> - - ); -}; diff --git a/packages/web/components/bridge/flow.ts b/packages/web/components/bridge/flow.ts deleted file mode 100644 index 336dd60122..0000000000 --- a/packages/web/components/bridge/flow.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Provider } from "react"; - -import { BridgeContext } from "~/hooks/bridge"; - -/** A bridge flow for UI for deposit/withdraw or fiat on ramping capable of handling the bridge context provider. */ -export interface BridgeFlowProvider { - Provider: Provider; -} diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx new file mode 100644 index 0000000000..bb79ba32b7 --- /dev/null +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -0,0 +1,472 @@ +import { Menu } from "@headlessui/react"; +import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import { MinimalAsset } from "@osmosis-labs/types"; +import { isNumeric, noop } from "@osmosis-labs/utils"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import { FunctionComponent, ReactNode, useState } from "react"; + +import { Icon } from "~/components/assets"; +import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; +import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; +import { InputBox } from "~/components/input"; +import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { Tooltip } from "~/components/tooltip"; +import { Button } from "~/components/ui/button"; +import { useConnectWalletModalRedirect, useTranslation } from "~/hooks"; +import { usePrice } from "~/hooks/queries/assets/use-price"; +import { useStore } from "~/stores"; +import { trimPlaceholderZeros } from "~/utils/number"; +import { api } from "~/utils/trpc"; + +interface AmountScreenProps { + type: "deposit" | "withdraw"; + + /** + * Includes both the canonical asset and its variants. + */ + assetsInOsmosis: MinimalAsset[] | undefined; +} + +export const AmountScreen = observer( + ({ type, assetsInOsmosis }: AmountScreenProps) => { + const { accountStore } = useStore(); + const wallet = accountStore.getWallet(accountStore.osmosisChainId); + const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); + const { t } = useTranslation(); + + const { accountActionButton: connectWalletButton, walletConnected } = + useConnectWalletModalRedirect( + { + className: "w-full", + }, + noop + ); + + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ + findChainNameOrId: "osmosis", + }); + const { data: nobleChain } = api.edge.chains.getChain.useQuery({ + findChainNameOrId: "noble", + }); + + const canonicalAsset = assetsInOsmosis?.[0]; + const { price: assetInOsmosisPrice, isLoading } = usePrice(canonicalAsset); + + const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); + const [cryptoAmount, setCryptoAmount] = useState("0"); + const [fiatAmount, setFiatAmount] = useState("0"); + + if ( + isLoading || + !assetsInOsmosis || + !canonicalAsset || + !assetInOsmosisPrice || + !osmosisChain || + !nobleChain + ) { + return ; + } + + const cryptoAmountPretty = new CoinPretty( + canonicalAsset, + cryptoAmount === "" + ? new Dec(0) + : new Dec(cryptoAmount).mul( + DecUtils.getTenExponentN(canonicalAsset.coinDecimals) + ) + ); + + const fiatAmountPretty = new PricePretty( + DEFAULT_VS_CURRENCY, + new Dec(fiatAmount === "" ? 0 : fiatAmount) + ); + + const parseFiatAmount = (value: string) => { + return value.replace("$", ""); + }; + + const formatFiatAmount = (value: string) => { + return `$${value}`; + }; + + const onInput = (type: "fiat" | "crypto") => (value: string) => { + let nextValue = type === "fiat" ? parseFiatAmount(value) : value; + if (!isNumeric(nextValue) && nextValue !== "") return; + + if (nextValue.startsWith("0") && !nextValue.startsWith("0.")) { + nextValue = nextValue.slice(1); + } + if (nextValue === "") { + nextValue = "0"; + } + if (nextValue === ".") { + nextValue = "0."; + } + + if (type === "fiat") { + // Update the crypto amount based on the fiat amount + const priceInFiat = assetInOsmosisPrice.toDec(); + const nextFiatAmount = new Dec(nextValue); + const nextCryptoAmount = nextFiatAmount.quo(priceInFiat).toString(); + + setCryptoAmount(trimPlaceholderZeros(nextCryptoAmount)); + } else { + // Update the fiat amount based on the crypto amount + const priceInFiat = assetInOsmosisPrice.toDec(); + const nextCryptoAmount = new Dec(nextValue); + const nextFiatAmount = nextCryptoAmount.mul(priceInFiat).toString(); + + setFiatAmount(trimPlaceholderZeros(nextFiatAmount)); + } + + type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); + }; + + return ( +
+
+ + {type === "deposit" + ? t("transfer.deposit") + : t("transfer.withdraw")} + {" "} + token image{" "} + {canonicalAsset.coinDenom} +
+ +
+
+ + {t("transfer.fromNetwork")} + + + + {t("transfer.toNetwork")} + +
+ +
+ + {nobleChain.pretty_name} + + + + + + {osmosisChain.pretty_name} + +
+
+ +
+
+
+
+ {inputUnit === "fiat" ? ( + <> + + + ) : ( +
+

+ {cryptoAmountPretty?.denom} +

+ +
+ )} +
+ +
+ + + +
+
+
+ +
+ {[ + { + label: "USDC.e", + amount: `$80.00 ${t("transfer.available")}`, + active: true, + }, + { label: "USDC", amount: "$30.00", active: false }, + { label: "USDC.axl", amount: "$10.00", active: false }, + ].map(({ label, amount, active }, index) => ( + + ))} +
+ +
+ + {type === "deposit" + ? t("transfer.transferWith") + : t("transfer.transferTo")} + +
+ {wallet?.walletInfo.prettyName} + {wallet?.walletInfo.prettyName} + +
+
+ + + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

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

+

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

+
+ } + > + + +
+ +
+ USDC + +
+
+ + + + + + + + + + +
+ )} + + +
+
+ + + {t("transfer.estimatingTime")} + +
+ + + {t("transfer.calculatingFees")} + +
+ +
+ {!walletConnected ? ( + connectWalletButton + ) : ( + <> + + + setIsMoreOptionsVisible(false)} + /> + + )} +
+
+ + ); + } +); + +const ChainSelectorButton: FunctionComponent<{ + readonly?: boolean; + children: ReactNode; + chainLogo: string; +}> = ({ readonly, children, chainLogo: _chainLogo }) => { + const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); + + if (readonly) { + return ( +
+ {children} +
+ ); + } + + return ( + <> + + setIsNetworkSelectVisible(false)} + /> + + ); +}; + +const AmountScreenSkeletonLoader = () => { + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/asset-select-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx new file mode 100644 index 0000000000..02bfebdb4c --- /dev/null +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -0,0 +1,225 @@ +import { MinimalAsset } from "@osmosis-labs/types"; +import classNames from "classnames"; +import debounce from "debounce"; +import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import React, { useMemo, useState } from "react"; + +import { Icon } from "~/components/assets"; +import { SearchBox } from "~/components/input"; +import { Intersection } from "~/components/intersection"; +import { Spinner } from "~/components/loaders"; +import { Tooltip } from "~/components/tooltip"; +import { + MainnetAssetSymbols, + MainnetVariantGroupKeys, + TestnetAssetSymbols, + TestnetVariantGroupKeys, +} from "~/config/generated/asset-lists"; +import { useTranslation } from "~/hooks"; +import { useShowPreviewAssets } from "~/hooks/use-show-preview-assets"; +import { ActivateUnverifiedTokenConfirmation } from "~/modals/activate-unverified-token-confirmation"; +import { useStore } from "~/stores"; +import { UnverifiedAssetsState } from "~/stores/user-settings/unverified-assets"; +import { formatPretty } from "~/utils/formatter"; +import { api, RouterOutputs } from "~/utils/trpc"; + +const variantsNotToBeExcluded = [ + "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", +] satisfies (MainnetVariantGroupKeys | TestnetVariantGroupKeys)[]; +const prioritizedDenoms = [ + "USDC", + "OSMO", + "ETH", + "SOL", + "USDT", + "WBTC", + "ATOM", + "TIA", +] satisfies (MainnetAssetSymbols | TestnetAssetSymbols)[]; + +interface AssetSelectScreenProps { + type: "deposit" | "withdraw"; + onSelectAsset: ( + asset: RouterOutputs["edge"]["assets"]["getImmersiveBridgeAssets"]["items"][number] + ) => void; +} + +export const AssetSelectScreen = observer( + ({ type, onSelectAsset }: AssetSelectScreenProps) => { + const { accountStore, userSettings } = useStore(); + const { showPreviewAssets } = useShowPreviewAssets(); + const { t } = useTranslation(); + + const wallet = accountStore.getWallet(accountStore.osmosisChainId); + + const [search, setSearch] = useState(""); + const [assetToActivate, setAssetToActivate] = useState( + null + ); + + const showUnverifiedAssetsSetting = + userSettings.getUserSettingById( + "unverified-assets" + ); + const shouldShowUnverifiedAssets = + showUnverifiedAssetsSetting?.state.showUnverifiedAssets; + + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + api.edge.assets.getImmersiveBridgeAssets.useInfiniteQuery( + { + type, + search: Boolean(search) + ? { + query: search, + } + : undefined, + userOsmoAddress: wallet?.address, + includePreview: showPreviewAssets, + variantsNotToBeExcluded, + prioritizedDenoms, + limit: 50, // items per page + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialCursor: 0, + + // avoid blocking + trpc: { + context: { + skipBatch: true, + }, + }, + } + ); + + const assets = useMemo( + () => data?.pages.flatMap((page) => page?.items) ?? [], + [data?.pages] + ); + const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; + + return ( +
+ { + if (!assetToActivate) return; + showUnverifiedAssetsSetting?.setState({ + showUnverifiedAssets: true, + }); + onSelectAsset(assetToActivate); + }} + onRequestClose={() => { + setAssetToActivate(null); + }} + /> + +

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

+ + { + setSearch(nextValue); + }, 300)} + className="my-4 flex-shrink-0" + placeholder={t("transfer.assetSelectScreen.searchAssets")} + size="full" + /> + +
+ {isLoading ? ( + <> + + + ) : ( + <> + {assets.map((asset) => ( + + ))} + { + if (canLoadMore) { + fetchNextPage(); + } + }} + /> + {isFetchingNextPage && ( +
+ +
+ )} + + )} +
+
+ ); + } +); diff --git a/packages/web/components/bridge/bridge-network-select.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx similarity index 69% rename from packages/web/components/bridge/bridge-network-select.tsx rename to packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index 7eb2baa33c..f2f5a4e8c2 100644 --- a/packages/web/components/bridge/bridge-network-select.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -4,45 +4,43 @@ import React, { useMemo, useState } from "react"; import { SearchBox } from "~/components/input"; import { Intersection } from "~/components/intersection"; import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; import { api } from "~/utils/trpc"; -export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { +export const BridgeNetworkSelectModal = (modalProps: ModalBaseProps) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(""); - const { - data: chainsPages, - hasNextPage, - isLoading, - isFetchingNextPage, - fetchNextPage, - } = api.edge.chains.getChains.useInfiniteQuery( - { - limit: 50, - search: query, - }, - { - enabled: modalProps.isOpen, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialCursor: 0, - keepPreviousData: true, + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + api.edge.chains.getChains.useInfiniteQuery( + { + limit: 50, + search: query, + }, + { + enabled: modalProps.isOpen, + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialCursor: 0, + keepPreviousData: true, - trpc: { - context: { - skipBatch: true, + trpc: { + context: { + skipBatch: true, + }, }, - }, - } - ); + } + ); const chains = useMemo( - () => chainsPages?.pages.flatMap((page) => page?.items) ?? [], - [chainsPages] + () => data?.pages.flatMap((page) => page?.items) ?? [], + [data] ); const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; return ( @@ -51,7 +49,7 @@ export const BridgeNetworkSelect = (modalProps: ModalBaseProps) => { setQuery(nextValue); }, 300)} className="my-4 flex-shrink-0" - placeholder="Search supported networks" + placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} size="full" />
diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx new file mode 100644 index 0000000000..a7f0df2aac --- /dev/null +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -0,0 +1,232 @@ +import { Transition } from "@headlessui/react"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import { memo, PropsWithChildren, useState } from "react"; +import { useLockBodyScroll } from "react-use"; + +import { Icon } from "~/components/assets"; +import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { AssetSelectScreen } from "~/components/bridge/immersive/asset-select-screen"; +import { Screen, ScreenManager } from "~/components/screen-manager"; +import { StepProgress } from "~/components/stepper/progress-bar"; +import { Button, IconButton } from "~/components/ui/button"; +import { EventName } from "~/config"; +import { useTranslation } from "~/hooks"; +import { BridgeFlowProvider } from "~/hooks/bridge"; +import { useAmplitudeAnalytics } from "~/hooks/use-amplitude-analytics"; +import { useDisclosure } from "~/hooks/use-disclosure"; +import { FiatRampKey } from "~/integrations"; +import { ModalCloseButton } from "~/modals"; +import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; +import { FiatRampsModal } from "~/modals/fiat-ramps"; +import { api } from "~/utils/trpc"; + +const enum ImmersiveBridgeScreens { + Asset = "0", + Amount = "1", + Review = "2", +} + +const MemoizedChildren = memo(({ children }: PropsWithChildren) => { + return <>{children}; +}); + +export const ImmersiveBridgeFlow = ({ + Provider, + children, +}: PropsWithChildren) => { + const { t } = useTranslation(); + + const [isVisible, setIsVisible] = useState(false); + const [step, setStep] = useState( + ImmersiveBridgeScreens.Asset + ); + const [direction, setDirection] = useState<"deposit" | "withdraw">("deposit"); + const { logEvent } = useAmplitudeAnalytics(); + + const [selectedAssetDenom, setSelectedAssetDenom] = useState(); + + const { data: canonicalAssetsWithVariants } = + api.edge.assets.getCanonicalAssetWithVariants.useQuery( + { + findMinDenomOrSymbol: selectedAssetDenom!, + }, + { + enabled: !isNil(selectedAssetDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); + + const [fiatRampParams, setFiatRampParams] = useState<{ + fiatRampKey: FiatRampKey; + assetKey: string; + } | null>(null); + + const { + isOpen: isFiatOnrampSelectionOpen, + onOpen: onOpenFiatOnrampSelection, + onClose: onCloseFiatOnrampSelection, + } = useDisclosure(); + + useLockBodyScroll(isVisible); + + const onClose = () => { + setIsVisible(false); + }; + + const onOpen = (direction: BridgeTransactionDirection) => { + setIsVisible(true); + setDirection(direction); + }; + + return ( + { + onOpen(direction); + }, + bridgeAsset: async ({ + anyDenom, + direction, + }: { + anyDenom: string; + direction: BridgeTransactionDirection; + }) => { + onOpen(direction); + setStep(ImmersiveBridgeScreens.Amount); + setSelectedAssetDenom(anyDenom); + }, + fiatRamp: ({ + fiatRampKey, + assetKey, + }: { + fiatRampKey: FiatRampKey; + assetKey: string; + }) => { + setFiatRampParams({ fiatRampKey, assetKey }); + }, + fiatRampSelection: onOpenFiatOnrampSelection, + }} + > + {children} + { + return setStep(screen as ImmersiveBridgeScreens); + }} + > + {() => ( + { + setSelectedAssetDenom(undefined); + setStep(ImmersiveBridgeScreens.Asset); + }} + > + onClose()} /> + {step !== ImmersiveBridgeScreens.Asset && ( + { + const previousStep = Number(step) - 1; + // @ts-expect-error + setStep(previousStep); + }} + className={ + "absolute left-8 top-[28px] z-50 w-fit text-osmoverse-400 hover:text-osmoverse-100" + } + icon={} + aria-label="Go Back" + /> + )} + +
+ setStep(ImmersiveBridgeScreens.Asset) + : undefined, + }, + { + displayLabel: t("transfer.stepLabels.amount"), + onClick: + step === ImmersiveBridgeScreens.Review + ? () => setStep(ImmersiveBridgeScreens.Amount) + : undefined, + }, + { + displayLabel: t("transfer.stepLabels.review"), + }, + ]} + currentStep={Number(step)} + /> + +
+ + {({ setCurrentScreen }) => ( + { + setCurrentScreen(ImmersiveBridgeScreens.Amount); + setSelectedAssetDenom(asset.coinDenom); + }} + /> + )} + + + + + + {({ goBack }) => ( +
+
Step 3: Review
+ + +
+ )} +
+
+
+
+ )} +
+ + {!isNil(fiatRampParams) && ( + { + setFiatRampParams(null); + }} + assetKey={fiatRampParams.assetKey} + fiatRampKey={fiatRampParams.fiatRampKey} + /> + )} + { + logEvent([EventName.ProfileModal.buyTokensClicked]); + }} + /> +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/index.ts b/packages/web/components/bridge/immersive/index.ts index 1a8f850f5f..d6609195e5 100644 --- a/packages/web/components/bridge/immersive/index.ts +++ b/packages/web/components/bridge/immersive/index.ts @@ -1 +1 @@ -export * from "./provider"; +export * from "./immersive-bridge"; diff --git a/packages/web/components/bridge/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx similarity index 89% rename from packages/web/components/bridge/more-bridge-options.tsx rename to packages/web/components/bridge/immersive/more-bridge-options.tsx index b0ab70311d..32c9f3b5e6 100644 --- a/packages/web/components/bridge/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -1,3 +1,4 @@ +import { MinimalAsset } from "@osmosis-labs/types"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import React from "react"; @@ -11,10 +12,11 @@ import { api } from "~/utils/trpc"; interface MoreBridgeOptionsProps extends ModalBaseProps { type: "deposit" | "withdraw"; + asset: MinimalAsset; } export const MoreBridgeOptions = observer( - ({ type, ...modalProps }: MoreBridgeOptionsProps) => { + ({ type, asset, ...modalProps }: MoreBridgeOptionsProps) => { const { accountStore, chainStore: { @@ -24,11 +26,7 @@ export const MoreBridgeOptions = observer( const wallet = accountStore.getWallet(accountStore.osmosisChainId); const { t } = useTranslation(); - // TODO: Use context state to get the fromAsset, toAsset, fromChain, and toChain - const { data: asset, isLoading: isLoadingAsset } = - api.edge.assets.getAssetWithPrice.useQuery({ - findMinDenomOrSymbol: "USDC", - }); + // TODO: Get fromChain, toChain, and address from props after supportTokens. const { data: externalUrlsData, isLoading: isLoadingExternalUrls } = api.bridgeTransfer.getExternalUrls.useQuery( { @@ -64,7 +62,7 @@ export const MoreBridgeOptions = observer( className="!max-w-[30rem]" {...modalProps} > -

+

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

+

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

Evm Address: {address}

- -
- ) : ( - - )} */} -
-
-
-
- ); -}; diff --git a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts new file mode 100644 index 0000000000..091d92929d --- /dev/null +++ b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts @@ -0,0 +1 @@ +export const useBridgeSupportedAssets = () => {}; diff --git a/packages/web/components/bridge/legacy.tsx b/packages/web/components/bridge/legacy.tsx index 5b9ea2539d..88148ff273 100644 --- a/packages/web/components/bridge/legacy.tsx +++ b/packages/web/components/bridge/legacy.tsx @@ -17,6 +17,7 @@ import { useTransferConfig, useWindowSize, } from "~/hooks"; +import { BridgeFlowProvider } from "~/hooks/bridge"; import { FiatRampKey } from "~/integrations"; import { ActivateUnverifiedTokenConfirmation, @@ -33,8 +34,6 @@ import { useStore } from "~/stores"; import { UnverifiedAssetsState } from "~/stores/user-settings"; import { removeQueryParam } from "~/utils/url"; -import { BridgeFlowProvider } from "./flow"; - const TransactionTypeQueryParamKey = "transaction_type"; const DenomQueryParamKey = "denom"; @@ -111,14 +110,20 @@ export const LegacyBridgeFlow = observer( ); const startBridge = useCallback( - (direction: "deposit" | "withdraw") => { + ({ direction }: { direction: "deposit" | "withdraw" }) => { transferConfig.startTransfer(direction); }, [transferConfig] ); const bridgeAsset = useCallback( - (anyDenom: string, direction: "deposit" | "withdraw") => { + ({ + anyDenom, + direction, + }: { + anyDenom: string; + direction: "deposit" | "withdraw"; + }) => { const balance = assetsStore.unverifiedIbcBalances.find( ({ balance }) => balance.denom === anyDenom || @@ -160,7 +165,13 @@ export const LegacyBridgeFlow = observer( ); const fiatRamp = useCallback( - (fiatRampKey: FiatRampKey, assetKey: string) => { + ({ + fiatRampKey, + assetKey, + }: { + fiatRampKey: FiatRampKey; + assetKey: string; + }) => { transferConfig.launchFiatRampsModal(fiatRampKey, assetKey); }, [transferConfig] @@ -194,7 +205,7 @@ export const LegacyBridgeFlow = observer( return; } - bridgeAsset(asset.balance.denom, direction); + bridgeAsset({ anyDenom: asset.balance.denom, direction }); removeQueryParam(TransactionTypeQueryParamKey); removeQueryParam(DenomQueryParamKey); }, [router.query, assetsStore.unverifiedIbcBalances, bridgeAsset]); diff --git a/packages/web/components/complex/assets-page-v1.tsx b/packages/web/components/complex/assets-page-v1.tsx index 08cfd8fb1d..424bde537d 100644 --- a/packages/web/components/complex/assets-page-v1.tsx +++ b/packages/web/components/complex/assets-page-v1.tsx @@ -60,13 +60,13 @@ export const AssetsPageV1: FunctionComponent = observer(() => { { label: t("assets.table.depositButton"), onClick: () => { - startBridge("deposit"); + startBridge({ direction: "deposit" }); }, }, { label: t("assets.table.withdrawButton"), onClick: () => { - startBridge("withdraw"); + startBridge({ direction: "withdraw" }); }, }, ], @@ -75,7 +75,7 @@ export const AssetsPageV1: FunctionComponent = observer(() => { const onTableDeposit = useCallback( (_chainId: string, coinDenom: string, externalDepositUrl?: string) => { if (!externalDepositUrl) { - bridgeAsset(coinDenom, "deposit"); + bridgeAsset({ anyDenom: coinDenom, direction: "deposit" }); } }, [bridgeAsset] @@ -83,7 +83,7 @@ export const AssetsPageV1: FunctionComponent = observer(() => { const onTableWithdraw = useCallback( (_chainId: string, coinDenom: string, externalWithdrawUrl?: string) => { if (!externalWithdrawUrl) { - bridgeAsset(coinDenom, "withdraw"); + bridgeAsset({ anyDenom: coinDenom, direction: "withdraw" }); } }, [bridgeAsset] diff --git a/packages/web/components/complex/portfolio-page.tsx b/packages/web/components/complex/portfolio-page.tsx index c31b262c4e..b25a8dacd2 100644 --- a/packages/web/components/complex/portfolio-page.tsx +++ b/packages/web/components/complex/portfolio-page.tsx @@ -59,13 +59,13 @@ export const PortfolioPage: FunctionComponent = () => { const onDeposit = useCallback( (coinMinimalDenom: string) => { - bridgeAsset(coinMinimalDenom, "deposit"); + bridgeAsset({ anyDenom: coinMinimalDenom, direction: "deposit" }); }, [bridgeAsset] ); const onWithdraw = useCallback( (coinMinimalDenom: string) => { - bridgeAsset(coinMinimalDenom, "withdraw"); + bridgeAsset({ anyDenom: coinMinimalDenom, direction: "withdraw" }); }, [bridgeAsset] ); @@ -173,7 +173,7 @@ const AssetsOverview: FunctionComponent<
+
); })}
diff --git a/packages/web/components/your-balance/your-balance.tsx b/packages/web/components/your-balance/your-balance.tsx index b55610a147..4dd0d835b8 100644 --- a/packages/web/components/your-balance/your-balance.tsx +++ b/packages/web/components/your-balance/your-balance.tsx @@ -480,7 +480,9 @@ const BalanceStats = observer(({ denom }: YourBalanceProps) => { @@ -513,7 +515,9 @@ const BalanceStats = observer(({ denom }: YourBalanceProps) => { data.amount.toDec().isZero() } variant="outline" - onClick={() => bridgeAsset(denom, "withdraw")} + onClick={() => + bridgeAsset({ anyDenom: denom, direction: "withdraw" }) + } > {t("assets.historyTable.colums.withdraw")} diff --git a/packages/web/config/generate-lists.ts b/packages/web/config/generate-lists.ts index 98c0b28319..6c7b58d900 100644 --- a/packages/web/config/generate-lists.ts +++ b/packages/web/config/generate-lists.ts @@ -18,6 +18,7 @@ import type { ChainList, IbcTransferMethod, } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; import { generateTsFile } from "~/utils/codegen"; @@ -251,6 +252,26 @@ async function generateAssetListFile({ .join(" | ")}; `; + content += ` + export type ${ + environment === "testnet" + ? "TestnetVariantGroupKeys" + : "MainnetVariantGroupKeys" + } = ${Array.from( + new Set(assetList.assets.map((asset) => asset.variantGroupKey)) + ) + .filter((groupKey, index, self) => { + if (isNil(groupKey)) { + return false; + } + + // remove duplicates + return self.indexOf(groupKey) === index; + }) + .map((groupKey) => `"${groupKey}"`) + .join(" | ")}; + `; + const success = await generateTsFile( content, codegenDir, diff --git a/packages/web/hooks/bridge.tsx b/packages/web/hooks/bridge.tsx index efc4919c0b..bede8085c0 100644 --- a/packages/web/hooks/bridge.tsx +++ b/packages/web/hooks/bridge.tsx @@ -1,4 +1,5 @@ -import { PropsWithChildren } from "react"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import { PropsWithChildren, Provider } from "react"; import { ImmersiveBridgeFlow } from "~/components/bridge/immersive"; import { LegacyBridgeFlow } from "~/components/bridge/legacy"; @@ -9,14 +10,21 @@ import { useFeatureFlags } from "./use-feature-flags"; export type BridgeContext = { /** Start bridging without knowing the asset to bridge yet. */ - startBridge: (direction: "deposit" | "withdraw") => void; + startBridge: (params: { direction: BridgeTransactionDirection }) => void; /** Start bridging a specified asset of coinMinimalDenom or symbol/denom. */ - bridgeAsset: (anyDenom: string, direction: "deposit" | "withdraw") => void; + bridgeAsset: (params: { + anyDenom: string; + direction: BridgeTransactionDirection; + }) => void; /** Open a specified fiat on ramp given a specific fiat ramp key and asset key. */ - fiatRamp: (fiatRampKey: FiatRampKey, assetKey: string) => void; + fiatRamp: (params: { fiatRampKey: FiatRampKey; assetKey: string }) => void; /** Open fiat ramp selection. */ fiatRampSelection: () => void; }; +/** A bridge flow for UI for deposit/withdraw or fiat on ramping capable of handling the bridge context provider. */ +export interface BridgeFlowProvider { + Provider: Provider; +} const [BridgeInnerProvider, useBridge] = createContext(); diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index 79b47e2d9e..691fbe607a 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -5,13 +5,13 @@ import { NotEnoughLiquidityError, NotEnoughQuotedError, } from "@osmosis-labs/pools"; -import type { Asset, RouterKey } from "@osmosis-labs/server"; +import type { RouterKey } from "@osmosis-labs/server"; import { makeSplitRoutesSwapExactAmountInMsg, makeSwapExactAmountInMsg, SignOptions, } from "@osmosis-labs/stores"; -import { Currency } from "@osmosis-labs/types"; +import { Currency, MinimalAsset } from "@osmosis-labs/types"; import { getAssetFromAssetList, isNil, @@ -661,7 +661,7 @@ export function useSwapAssets({ Boolean(fromAssetDenom) && Boolean(toAssetDenom) && useOtherCurrencies; - // use a separate query for search to maintain pagination in other infinite query + const { data: selectableAssetPages, isLoading: isLoadingSelectAssets, @@ -966,7 +966,7 @@ function useToFromDenoms({ /** Will query for an individual asset of any type of denom (symbol, min denom) * if it's not already in the list of existing assets. */ -function useSwapAsset({ +function useSwapAsset({ minDenomOrSymbol, existingAssets = [], }: { @@ -1013,12 +1013,12 @@ function getSwapTxParameters({ quote: | RouterOutputs["local"]["quoteRouter"]["routeTokenOutGivenIn"] | undefined; - fromAsset: Asset & + fromAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - toAsset: Asset & + toAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; @@ -1100,12 +1100,12 @@ function getSwapMessages({ quote: | RouterOutputs["local"]["quoteRouter"]["routeTokenOutGivenIn"] | undefined; - fromAsset: Asset & + fromAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - toAsset: Asset & + toAsset: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; @@ -1161,12 +1161,12 @@ function useQueryRouterBestQuote( RouterInputs["local"]["quoteRouter"]["routeTokenOutGivenIn"], "preferredRouter" | "tokenInDenom" | "tokenOutDenom" > & { - tokenIn: Asset & + tokenIn: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; }>; - tokenOut: Asset & + tokenOut: MinimalAsset & Partial<{ amount: CoinPretty; usdValue: PricePretty; diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 58a06e4200..bcb0e33d76 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Weitere Auszahlungsoptionen", "convertTo": "Konvertieren zu", "recommended": "Empfohlen", + "stepLabels": { + "asset": "Vermögenswert", + "amount": "Menge", + "review": "Rezension" + }, "moreBridgeOptions": { "titleDeposit": "Mehr Einzahlungsmöglichkeiten", "titleWithdraw": "Weitere Auszahlungsoptionen", @@ -930,6 +935,15 @@ "depositWith": "Einzahlung mit", "withdrawWith": "Abheben mit" }, + "assetSelectScreen": { + "titleDeposit": "Wählen Sie einen Vermögenswert zur Einzahlung aus", + "titleWithdraw": "Wählen Sie einen Vermögenswert zum Abheben aus", + "searchAssets": "Assets suchen" + }, + "bridgeNetworkSelect": { + "title": "Netzwerk auswählen", + "searchPlaceholder": "Unterstützte Netzwerke durchsuchen" + }, "pendingWithdraw": "Ausstehender Auszug", "pendingDeposit": "Ausstehende Einzahlung", "completedWithdraw": "Auszahlung abgeschlossen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index af2c7b1c49..f500494907 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "More withdraw options", "convertTo": "Convert to", "recommended": "Recommended", + "stepLabels": { + "asset": "Asset", + "amount": "Amount", + "review": "Review" + }, "moreBridgeOptions": { "titleDeposit": "More deposit options", "titleWithdraw": "More withdraw options", @@ -930,6 +935,15 @@ "depositWith": "Deposit with", "withdrawWith": "Withdraw with" }, + "assetSelectScreen": { + "titleDeposit": "Select an asset to deposit", + "titleWithdraw": "Select an asset to withdraw", + "searchAssets": "Search assets" + }, + "bridgeNetworkSelect": { + "title": "Select network", + "searchPlaceholder": "Search supported networks" + }, "pendingWithdraw": "Pending withdraw", "pendingDeposit": "Pending deposit", "completedWithdraw": "Withdraw completed", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index e2b91cf386..c0d0136671 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Más opciones de retiro", "convertTo": "Convertir a", "recommended": "Recomendado", + "stepLabels": { + "asset": "Activo", + "amount": "Cantidad", + "review": "Revisar" + }, "moreBridgeOptions": { "titleDeposit": "Más opciones de depósito", "titleWithdraw": "Más opciones de retiro", @@ -930,6 +935,15 @@ "depositWith": "Depositar con", "withdrawWith": "Retirarse con" }, + "assetSelectScreen": { + "titleDeposit": "Seleccione un activo para depositar", + "titleWithdraw": "Seleccione un activo para retirar", + "searchAssets": "Buscar activos" + }, + "bridgeNetworkSelect": { + "title": "Seleccione la red", + "searchPlaceholder": "Buscar redes compatibles" + }, "pendingWithdraw": "A la espera de Retiro", "pendingDeposit": "Deposito pendiente", "completedWithdraw": "Retiro completado", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 51856c12f3..e2e13bbb6e 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "گزینه های برداشت بیشتر", "convertTo": "تبدیل به", "recommended": "توصیه شده", + "stepLabels": { + "asset": "دارایی", + "amount": "میزان", + "review": "مرور" + }, "moreBridgeOptions": { "titleDeposit": "گزینه های سپرده بیشتر", "titleWithdraw": "گزینه های برداشت بیشتر", @@ -930,6 +935,15 @@ "depositWith": "سپرده گذاری با", "withdrawWith": "برداشت با" }, + "assetSelectScreen": { + "titleDeposit": "دارایی را برای سپرده گذاری انتخاب کنید", + "titleWithdraw": "دارایی را برای برداشت انتخاب کنید", + "searchAssets": "جستجوی دارایی ها" + }, + "bridgeNetworkSelect": { + "title": "شبکه را انتخاب کنید", + "searchPlaceholder": "جستجوی شبکه های پشتیبانی شده" + }, "pendingWithdraw": "در انتظار برداشت", "pendingDeposit": "سپرده معلق", "completedWithdraw": "برداشت کامل شد", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index 18fad289c1..c3a1cf11b7 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Plus d'options de retrait", "convertTo": "Convertir en", "recommended": "Recommandé", + "stepLabels": { + "asset": "Actif", + "amount": "Montant", + "review": "Revoir" + }, "moreBridgeOptions": { "titleDeposit": "Plus d'options de dépôt", "titleWithdraw": "Plus d'options de retrait", @@ -930,6 +935,15 @@ "depositWith": "Dépôt avec", "withdrawWith": "Retirer avec" }, + "assetSelectScreen": { + "titleDeposit": "Sélectionnez un actif à déposer", + "titleWithdraw": "Sélectionnez un actif à retirer", + "searchAssets": "Rechercher des ressources" + }, + "bridgeNetworkSelect": { + "title": "Sélectionnez réseau", + "searchPlaceholder": "Rechercher des réseaux pris en charge" + }, "pendingWithdraw": "En attente de retrait", "pendingDeposit": "Dépôt en attente", "completedWithdraw": "Retrait terminé", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 419cbfc598..310d9cdc09 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "વધુ ઉપાડ વિકલ્પો", "convertTo": "માં કન્વર્ટ કરો", "recommended": "ભલામણ કરેલ", + "stepLabels": { + "asset": "એસેટ", + "amount": "રકમ", + "review": "સમીક્ષા" + }, "moreBridgeOptions": { "titleDeposit": "વધુ થાપણ વિકલ્પો", "titleWithdraw": "વધુ ઉપાડ વિકલ્પો", @@ -930,6 +935,15 @@ "depositWith": "સાથે જમા", "withdrawWith": "સાથે પાછી ખેંચો" }, + "assetSelectScreen": { + "titleDeposit": "જમા કરવા માટે સંપત્તિ પસંદ કરો", + "titleWithdraw": "ઉપાડવા માટે સંપત્તિ પસંદ કરો", + "searchAssets": "સંપત્તિ શોધો" + }, + "bridgeNetworkSelect": { + "title": "નેટવર્ક પસંદ કરો", + "searchPlaceholder": "સપોર્ટેડ નેટવર્ક્સ શોધો" + }, "pendingWithdraw": "બાકી ઉપાડ", "pendingDeposit": "બાકી થાપણ", "completedWithdraw": "ઉપાડ પૂર્ણ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 51cacf89cb..4573aeb1d0 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "अधिक निकासी विकल्प", "convertTo": "में बदलो", "recommended": "अनुशंसित", + "stepLabels": { + "asset": "संपत्ति", + "amount": "मात्रा", + "review": "समीक्षा" + }, "moreBridgeOptions": { "titleDeposit": "अधिक जमा विकल्प", "titleWithdraw": "अधिक निकासी विकल्प", @@ -930,6 +935,15 @@ "depositWith": "जमा करें", "withdrawWith": "साथ वापस लें" }, + "assetSelectScreen": { + "titleDeposit": "जमा करने के लिए एक परिसंपत्ति का चयन करें", + "titleWithdraw": "निकासी के लिए एक परिसंपत्ति का चयन करें", + "searchAssets": "संपत्ति खोजें" + }, + "bridgeNetworkSelect": { + "title": "नेटवर्क चुनें", + "searchPlaceholder": "समर्थित नेटवर्क खोजें" + }, "pendingWithdraw": "लंबित निकासी", "pendingDeposit": "लंबित जमा", "completedWithdraw": "निकासी पूर्ण हुई", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 15d8c725e0..780bbdd5e1 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "引き出しオプションの追加", "convertTo": "に変換", "recommended": "推奨", + "stepLabels": { + "asset": "資産", + "amount": "額", + "review": "レビュー" + }, "moreBridgeOptions": { "titleDeposit": "その他の入金オプション", "titleWithdraw": "引き出しオプションの追加", @@ -930,6 +935,15 @@ "depositWith": "入金", "withdrawWith": "撤退する" }, + "assetSelectScreen": { + "titleDeposit": "預ける資産を選択してください", + "titleWithdraw": "引き出す資産を選択してください", + "searchAssets": "アセットを検索" + }, + "bridgeNetworkSelect": { + "title": "ネットワークを選択", + "searchPlaceholder": "サポートされているネットワークを検索" + }, "pendingWithdraw": "保留中の撤回", "pendingDeposit": "保留中の入金", "completedWithdraw": "引き出しが完了しました", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 68741be0b5..6145719261 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "더 많은 인출 옵션", "convertTo": "로 변환하다", "recommended": "추천", + "stepLabels": { + "asset": "유산", + "amount": "양", + "review": "검토" + }, "moreBridgeOptions": { "titleDeposit": "더 많은 입금 옵션", "titleWithdraw": "더 많은 인출 옵션", @@ -930,6 +935,15 @@ "depositWith": "다음으로 입금", "withdrawWith": "다음으로 인출" }, + "assetSelectScreen": { + "titleDeposit": "입금할 자산을 선택하세요", + "titleWithdraw": "출금할 자산을 선택하세요", + "searchAssets": "자산 검색" + }, + "bridgeNetworkSelect": { + "title": "네트워크 선택", + "searchPlaceholder": "지원되는 네트워크 검색" + }, "pendingWithdraw": "출금 대기 중", "pendingDeposit": "입금 대기 중", "completedWithdraw": "출금 완료", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index a9b7b4adad..f3f170fa77 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Więcej opcji wypłat", "convertTo": "Konwertuj na", "recommended": "Zalecana", + "stepLabels": { + "asset": "Zaleta", + "amount": "Kwota", + "review": "Recenzja" + }, "moreBridgeOptions": { "titleDeposit": "Więcej opcji depozytu", "titleWithdraw": "Więcej opcji wypłat", @@ -930,6 +935,15 @@ "depositWith": "Wpłać za pomocą", "withdrawWith": "Wycofaj się z" }, + "assetSelectScreen": { + "titleDeposit": "Wybierz zasób do zdeponowania", + "titleWithdraw": "Wybierz zasób do wypłaty", + "searchAssets": "Wyszukaj zasoby" + }, + "bridgeNetworkSelect": { + "title": "Wybierz sieć", + "searchPlaceholder": "Wyszukaj obsługiwane sieci" + }, "pendingWithdraw": "Oczekuje na wycofanie", "pendingDeposit": "Oczekujący depozyt", "completedWithdraw": "Wypłata zakończona", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 15923601d2..2672139ca9 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Mais opções de retirada", "convertTo": "Converter para", "recommended": "Recomendado", + "stepLabels": { + "asset": "Ativo", + "amount": "Quantia", + "review": "Análise" + }, "moreBridgeOptions": { "titleDeposit": "Mais opções de depósito", "titleWithdraw": "Mais opções de retirada", @@ -930,6 +935,15 @@ "depositWith": "Deposite com", "withdrawWith": "Retirar com" }, + "assetSelectScreen": { + "titleDeposit": "Selecione um ativo para depositar", + "titleWithdraw": "Selecione um ativo para retirar", + "searchAssets": "Pesquisar ativos" + }, + "bridgeNetworkSelect": { + "title": "Selecione a rede", + "searchPlaceholder": "Pesquisar redes suportadas" + }, "pendingWithdraw": "Retirada pendente", "pendingDeposit": "Depósito pendente", "completedWithdraw": "Retirada concluída", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index d645c0421d..f0fbfbdd0c 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Mai multe opțiuni de retragere", "convertTo": "Schimba in", "recommended": "Recomandat", + "stepLabels": { + "asset": "Atu", + "amount": "Cantitate", + "review": "Revizuire" + }, "moreBridgeOptions": { "titleDeposit": "Mai multe opțiuni de depunere", "titleWithdraw": "Mai multe opțiuni de retragere", @@ -930,6 +935,15 @@ "depositWith": "Depozit cu", "withdrawWith": "Retrage cu" }, + "assetSelectScreen": { + "titleDeposit": "Selectați un activ de depus", + "titleWithdraw": "Selectați un activ de retras", + "searchAssets": "Căutați active" + }, + "bridgeNetworkSelect": { + "title": "Selecteaza reteaua", + "searchPlaceholder": "Căutați rețele acceptate" + }, "pendingWithdraw": "În așteptarea retragerii", "pendingDeposit": "Depunerea în așteptare", "completedWithdraw": "Retragere finalizată", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 71d39ead12..711873b12b 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Больше вариантов вывода", "convertTo": "Перевести в", "recommended": "рекомендуемые", + "stepLabels": { + "asset": "Объект", + "amount": "Количество", + "review": "Обзор" + }, "moreBridgeOptions": { "titleDeposit": "Больше вариантов депозита", "titleWithdraw": "Больше вариантов вывода", @@ -930,6 +935,15 @@ "depositWith": "Депозит с", "withdrawWith": "Вывод средств с помощью" }, + "assetSelectScreen": { + "titleDeposit": "Выберите актив для депозита", + "titleWithdraw": "Выберите актив для вывода", + "searchAssets": "Поиск активов" + }, + "bridgeNetworkSelect": { + "title": "Выберите сеть", + "searchPlaceholder": "Поиск поддерживаемых сетей" + }, "pendingWithdraw": "Ожидание вывода", "pendingDeposit": "Ожидается депозит", "completedWithdraw": "Вывод завершен", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 5fe0bea91b..ea0bcb1a63 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "Daha fazla para çekme seçeneği", "convertTo": "E dönüşmek", "recommended": "Tavsiye edilen", + "stepLabels": { + "asset": "Varlık", + "amount": "Miktar", + "review": "Gözden geçirmek" + }, "moreBridgeOptions": { "titleDeposit": "Daha fazla para yatırma seçeneği", "titleWithdraw": "Daha fazla para çekme seçeneği", @@ -930,6 +935,15 @@ "depositWith": "Şununla para yatır:", "withdrawWith": "Şununla çekil:" }, + "assetSelectScreen": { + "titleDeposit": "Yatırılacak bir varlık seçin", + "titleWithdraw": "Çekilecek bir varlık seçin", + "searchAssets": "Varlıkları arayın" + }, + "bridgeNetworkSelect": { + "title": "Ağ seçin", + "searchPlaceholder": "Desteklenen ağları arayın" + }, "pendingWithdraw": "Geri çekilme bekleniyor", "pendingDeposit": "Bekleyen mevduat", "completedWithdraw": "Para çekme işlemi tamamlandı", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index f97b5995d0..0f7a530c65 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款选项", "convertTo": "转换成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "资产", + "amount": "数量", + "review": "审查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款选择", "titleWithdraw": "更多提款选项", @@ -930,6 +935,15 @@ "depositWith": "存款", "withdrawWith": "提款" }, + "assetSelectScreen": { + "titleDeposit": "选择要存入的资产", + "titleWithdraw": "选择要提取的资产", + "searchAssets": "搜索资产" + }, + "bridgeNetworkSelect": { + "title": "选择网络", + "searchPlaceholder": "搜索支持的网络" + }, "pendingWithdraw": "等待提款", "pendingDeposit": "待存款", "completedWithdraw": "提现完成", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index da80f75b56..f6482124e1 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "資產", + "amount": "數量", + "review": "審查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款選擇", "titleWithdraw": "更多提款選項", @@ -930,6 +935,15 @@ "depositWith": "存款於", "withdrawWith": "撤回與" }, + "assetSelectScreen": { + "titleDeposit": "選擇要存入的資產", + "titleWithdraw": "選擇要提取的資產", + "searchAssets": "搜尋資產" + }, + "bridgeNetworkSelect": { + "title": "選擇網路", + "searchPlaceholder": "搜尋支援的網絡" + }, "pendingWithdraw": "待提款", "pendingDeposit": "待定存款", "completedWithdraw": "提現完成", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 364d01c7cf..7d53cf46f8 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -922,6 +922,11 @@ "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", "recommended": "受到推崇的", + "stepLabels": { + "asset": "資產", + "amount": "數量", + "review": "審查" + }, "moreBridgeOptions": { "titleDeposit": "更多存款選擇", "titleWithdraw": "更多提款選項", @@ -930,6 +935,15 @@ "depositWith": "存款於", "withdrawWith": "撤回與" }, + "assetSelectScreen": { + "titleDeposit": "選擇要存入的資產", + "titleWithdraw": "選擇要提取的資產", + "searchAssets": "搜尋資產" + }, + "bridgeNetworkSelect": { + "title": "選擇網路", + "searchPlaceholder": "搜尋支援的網絡" + }, "pendingWithdraw": "待提款", "pendingDeposit": "待定存款", "completedWithdraw": "提現完成", diff --git a/packages/web/modals/fiat-on-ramp-selection.tsx b/packages/web/modals/fiat-on-ramp-selection.tsx index 0198adc963..29c591eecf 100644 --- a/packages/web/modals/fiat-on-ramp-selection.tsx +++ b/packages/web/modals/fiat-on-ramp-selection.tsx @@ -58,7 +58,7 @@ export const FiatOnrampSelectionModal: FunctionComponent< className="flex h-28 items-center !justify-start gap-2 !bg-osmoverse-900 px-5 py-5 transition-colors hover:!bg-osmoverse-700" onClick={() => { onSelectRamp?.(rampKey); - fiatRamp(rampKey, initialAsset); + fiatRamp({ fiatRampKey: rampKey, assetKey: initialAsset }); modalProps.onRequestClose(); }} > From e2fc50ca4b9c9a037e294dc63b9c57e36e3d5dfb Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:09:13 +0200 Subject: [PATCH 11/15] Added a job to delete deployments created by monitoring (#3421) * Added a job to delete deployments --- .github/workflows/monitoring-e2e-tests.yml | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/monitoring-e2e-tests.yml b/.github/workflows/monitoring-e2e-tests.yml index 6d2185836e..69e67b9b75 100644 --- a/.github/workflows/monitoring-e2e-tests.yml +++ b/.github/workflows/monitoring-e2e-tests.yml @@ -204,3 +204,33 @@ jobs: with: name: ${{ matrix.env }}-quote-test-results path: packages/web/playwright-report + + delete-deployments: + runs-on: ubuntu-latest + needs: frontend-e2e-tests + steps: + - name: Delete Previous deployments + uses: actions/github-script@v7 + with: + debug: true + script: | + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); From 2147cc3c7764e9597e8b2faef4f0e2cd71f50216 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Mon, 1 Jul 2024 18:16:43 -0400 Subject: [PATCH 12/15] (Deposit/Withdrawal) Fetch Bridge Provider Quote (#3402) * feat: display immersive bridge assets * feat: display selected asset * feat: add skeleton loader to amount screen * feat: handle back button, and improve amount input * fix: pagination test * fix: type * fix: more bridge options * improvement: @jonator feedback * improvement: @jonator feedback * feat: aggregate chains and assets * feat: merge assets by chain id * feat: aggregate assets display them initially on amount screen * feat: add local bridge transfer router * feat: use supported asset as source asset * improvement: type fix * feat: handle wallets in immersive bridge * feat: handle connection edge cases * feat: set up wallet connection from immersive bridge * feat: create initial use bridge quote hook * improvement: @jonator feedback * feat: remove source denom from minimal asset * test: update snapshots * feat: receive quote from input * fix: build * feat: display quote remaining time * feat: add provider details * test: update snapshot * fix: translation * feat: add bridge provider dropdown * feat: add details icons and tooltips * fix build * fix tests & build * feat: add amount and confirmation screen --------- Co-authored-by: Jon Ator --- packages/bridge/package.json | 2 +- .../__tests__/axelar-bridge-provider.spec.ts | 2 +- packages/bridge/src/axelar/index.ts | 7 +- packages/bridge/src/axelar/tokens.ts | 6 +- packages/bridge/src/chain.ts | 22 - packages/bridge/src/index.ts | 1 - packages/bridge/src/interface.ts | 9 +- packages/bridge/src/skip/index.ts | 12 +- packages/bridge/src/squid/index.ts | 9 +- packages/server/package.json | 3 +- .../complex/assets/__tests__/assets.spec.ts | 22 +- .../src/queries/complex/assets/ethereum.ts | 40 + .../src/queries/complex/assets/index.ts | 3 +- .../server/src/queries/complex/assets/user.ts | 16 +- packages/trpc/src/assets.ts | 12 +- packages/trpc/src/chains.ts | 26 +- packages/trpc/src/parameter-types.ts | 10 + packages/types/src/chain-types.ts | 10 + packages/utils/package.json | 3 +- packages/{bridge => utils}/src/ethereum.ts | 47 +- packages/utils/src/index.ts | 1 + packages/web/__tests__/index-page.spec.tsx | 15 +- .../amount-and-confirmation-screen.tsx | 126 ++ .../bridge/immersive/amount-screen.tsx | 1199 ++++++++++++++--- .../bridge/immersive/asset-select-screen.tsx | 4 +- .../immersive/bridge-network-select-modal.tsx | 239 +++- .../immersive/bridge-provider-dropdown.tsx | 143 ++ .../immersive/bridge-quote-remaining-time.tsx | 77 ++ .../immersive/bridge-wallet-select-modal.tsx | 273 ++++ .../bridge/immersive/immersive-bridge.tsx | 39 +- .../bridge/immersive/more-bridge-options.tsx | 63 +- .../bridge/immersive/use-bridge-quote.ts | 674 +++++++++ .../immersive/use-bridge-supported-assets.ts | 1 - .../immersive/use-bridges-supported-assets.ts | 201 +++ .../web/components/buttons/icon-button.tsx | 2 + packages/web/components/drawers/drawer.tsx | 8 +- .../swap-tool/__tests__/swap-tool.spec.tsx | 10 + .../wallet-states/switching-network-state.tsx | 40 + packages/web/config/generate-lists.ts | 8 +- packages/web/hooks/evm-wallet.ts | 9 +- packages/web/hooks/use-feature-flags.ts | 1 - packages/web/integrations/axelar/transfer.tsx | 5 +- packages/web/integrations/axelar/types.ts | 9 +- packages/web/integrations/axelar/utils.ts | 4 +- packages/web/integrations/ethereum/types.ts | 7 - packages/web/integrations/nomic/transfer.tsx | 4 +- packages/web/localizations/de.json | 20 +- packages/web/localizations/en.json | 20 +- packages/web/localizations/es.json | 20 +- packages/web/localizations/fa.json | 20 +- packages/web/localizations/fr.json | 20 +- packages/web/localizations/gu.json | 20 +- packages/web/localizations/hi.json | 20 +- packages/web/localizations/ja.json | 20 +- packages/web/localizations/ko.json | 20 +- packages/web/localizations/pl.json | 20 +- packages/web/localizations/pt-br.json | 20 +- packages/web/localizations/ro.json | 20 +- packages/web/localizations/ru.json | 20 +- packages/web/localizations/tr.json | 20 +- packages/web/localizations/zh-cn.json | 20 +- packages/web/localizations/zh-hk.json | 20 +- packages/web/localizations/zh-tw.json | 20 +- packages/web/modals/bridge-transfer-v1.tsx | 4 +- packages/web/modals/bridge-transfer-v2.tsx | 14 +- packages/web/modals/select-asset-source.tsx | 4 +- packages/web/modals/transfer-asset-select.tsx | 6 +- .../wallet-select/cosmos-wallet-state.tsx | 3 +- .../modals/wallet-select/evm-wallet-state.tsx | 2 +- .../modals/wallet-select/full-wallet-list.tsx | 3 +- packages/web/modals/wallet-select/index.tsx | 225 +--- .../wallet-select/simple-wallet-list.tsx | 2 +- .../wallet-select/use-connect-wallet.ts | 214 +++ packages/web/modals/wallet-select/utils.ts | 19 +- packages/web/package.json | 6 +- packages/web/pages/test-bridge.tsx | 37 + packages/web/public/icons/sprite.svg | 11 + packages/web/server/api/local-router.ts | 3 + .../web/server/api/routers/bridge-transfer.ts | 109 +- .../api/routers/local-bridge-transfer.ts | 217 +++ .../web/stores/assets/transfer-ui-config.ts | 12 +- packages/web/utils/ethereum.ts | 40 + yarn.lock | 245 +++- 83 files changed, 4161 insertions(+), 779 deletions(-) delete mode 100644 packages/bridge/src/chain.ts create mode 100644 packages/server/src/queries/complex/assets/ethereum.ts rename packages/{bridge => utils}/src/ethereum.ts (81%) create mode 100644 packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx create mode 100644 packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx create mode 100644 packages/web/components/bridge/immersive/bridge-quote-remaining-time.tsx create mode 100644 packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx create mode 100644 packages/web/components/bridge/immersive/use-bridge-quote.ts delete mode 100644 packages/web/components/bridge/immersive/use-bridge-supported-assets.ts create mode 100644 packages/web/components/bridge/immersive/use-bridges-supported-assets.ts create mode 100644 packages/web/components/wallet-states/switching-network-state.tsx create mode 100644 packages/web/modals/wallet-select/use-connect-wallet.ts create mode 100644 packages/web/pages/test-bridge.tsx create mode 100644 packages/web/server/api/routers/local-bridge-transfer.ts create mode 100644 packages/web/utils/ethereum.ts diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 4173bc14e3..443266043e 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -38,7 +38,7 @@ "cachified": "^3.5.4", "long": "^5.2.3", "lru-cache": "^10.0.1", - "viem": "2.13.3", + "viem": "2.16.4", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts index 75344f4eb7..9344955015 100644 --- a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts +++ b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts @@ -4,6 +4,7 @@ import { AxelarQueryAPI, } from "@axelar-network/axelarjs-sdk"; import { estimateGasFee } from "@osmosis-labs/tx"; +import { NativeEVMTokenConstantAddress } from "@osmosis-labs/utils"; import { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; // eslint-disable-next-line import/no-extraneous-dependencies @@ -11,7 +12,6 @@ import { rest } from "msw"; import { MockAssetLists } from "../../__tests__/mock-asset-lists"; import { server } from "../../__tests__/msw"; -import { NativeEVMTokenConstantAddress } from "../../ethereum"; import { BridgeProviderContext } from "../../interface"; import { AxelarBridgeProvider } from "../index"; import { diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index dadb7e0a02..49f635e2af 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -7,7 +7,11 @@ import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import type { IbcTransferMethod } from "@osmosis-labs/types"; -import { getAssetFromAssetList } from "@osmosis-labs/utils"; +import { + EthereumChainInfo, + getAssetFromAssetList, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import { cachified } from "cachified"; import { Address, @@ -19,7 +23,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, diff --git a/packages/bridge/src/axelar/tokens.ts b/packages/bridge/src/axelar/tokens.ts index f2c7b5e43d..dc6f597fe9 100644 --- a/packages/bridge/src/axelar/tokens.ts +++ b/packages/bridge/src/axelar/tokens.ts @@ -1,11 +1,11 @@ -import { SourceChain } from "../chain"; -import { EthereumChainInfo } from "../ethereum"; +import { AxelarSourceChain, EthereumChainInfo } from "@osmosis-labs/utils"; + import { BridgeEnvironment } from "../interface"; /** @deprecated Prefer using Axelar chain/asset list API via bridge providers instead */ export type SourceChainTokenConfig = { /** Source Chain identifier. */ - id: SourceChain; + id: AxelarSourceChain; chainId?: number; /** Address of origin ERC20 token for that origin chain. Leave blank to * prefer native ETH currency if `id` is not a Cosmos chain in `ChainInfo`. diff --git a/packages/bridge/src/chain.ts b/packages/bridge/src/chain.ts deleted file mode 100644 index 8a2d9d7ba2..0000000000 --- a/packages/bridge/src/chain.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** Human-displayable global source chain identifiers. - * TODO: use global chain IDs instead of display names as keys - */ -export type SourceChain = - | "Bitcoin" - | "Bitcoin Testnet" - | "Aurora Testnet" - | "Avalanche" - | "Avalanche Fuji Testnet" - | "Binance Smart Chain" - | "BSC Testnet" - | "Ethereum" - | "Goerli Testnet" - | "Fantom" - | "Fantom Testnet" - | "Moonbeam" - | "Moonbase Alpha" - | "Polygon" - | "Mumbai" - | "Filecoin" - | "Filecoin Hyperspace" - | "Arbitrum"; diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts index c9116a9098..8ab54e4c8b 100644 --- a/packages/bridge/src/index.ts +++ b/packages/bridge/src/index.ts @@ -1,6 +1,5 @@ export * from "./axelar"; export * from "./bridge-providers"; -export * from "./chain"; export * from "./errors"; export * from "./ibc"; export * from "./interface"; diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index f4b791278e..4a7b998823 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -139,7 +139,10 @@ const evmChainSchema = z.object({ chainType: z.literal("evm"), }); -const bridgeChainSchema = z.union([cosmosChainSchema, evmChainSchema]); +export const bridgeChainSchema = z.discriminatedUnion("chainType", [ + cosmosChainSchema, + evmChainSchema, +]); export type BridgeChain = z.infer; @@ -154,7 +157,7 @@ export interface BridgeStatus { maintenanceMessage?: string; } -const bridgeAssetSchema = z.object({ +export const bridgeAssetSchema = z.object({ /** * The displayable denomination of the asset. */ @@ -171,7 +174,7 @@ const bridgeAssetSchema = z.object({ export type BridgeAsset = z.infer; -const getBridgeSupportedAssetsParams = z.object({ +export const getBridgeSupportedAssetsParams = z.object({ /** * The originating chain information. */ diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index e27f4da8d1..bd615c8ae8 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -3,7 +3,11 @@ import { Registry } from "@cosmjs/proto-signing"; import { ibcProtoRegistry } from "@osmosis-labs/proto-codecs"; import { estimateGasFee } from "@osmosis-labs/tx"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; -import { isNil } from "@osmosis-labs/utils"; +import { + EthereumChainInfo, + isNil, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import cachified from "cachified"; import { Address, @@ -18,7 +22,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, @@ -311,6 +314,7 @@ export class SkipBridgeProvider implements BridgeProvider { ...chainInfo, address: sharedOriginAsset.denom, denom: + sharedOriginAsset.recommended_symbol ?? sharedOriginAsset.symbol ?? sharedOriginAsset.name ?? sharedOriginAsset.denom, @@ -756,9 +760,9 @@ export class SkipBridgeProvider implements BridgeProvider { const url = new URL("https://ibc.fun/"); url.searchParams.set("src_chain", String(fromChain.chainId)); - url.searchParams.set("src_asset", fromAsset.address); + url.searchParams.set("src_asset", fromAsset.address.toLowerCase()); url.searchParams.set("dest_chain", String(toChain.chainId)); - url.searchParams.set("dest_asset", toAsset.address); + url.searchParams.set("dest_asset", toAsset.address.toLowerCase()); return { urlProviderName: "IBC.fun", url }; } diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 4caa6c9fe6..28026d090d 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -8,7 +8,13 @@ import { } from "@0xsquid/sdk"; import { Dec } from "@keplr-wallet/unit"; import { CosmosCounterparty, EVMCounterparty } from "@osmosis-labs/types"; -import { apiClient, ApiClientError, isNil } from "@osmosis-labs/utils"; +import { + apiClient, + ApiClientError, + EthereumChainInfo, + isNil, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import { cachified } from "cachified"; import Long from "long"; import { @@ -21,7 +27,6 @@ import { } from "viem"; import { BridgeQuoteError } from "../errors"; -import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, diff --git a/packages/server/package.json b/packages/server/package.json index 049e2b499b..2492fc2c75 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,8 @@ "jest-util": "^29.7.0", "lru-cache": "^10.0.1", "superjson": "^2.2.1", - "zod": "^3.22.4" + "zod": "^3.22.4", + "viem": "2.16.4" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts index 90ea9deea3..b93f617acb 100644 --- a/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts +++ b/packages/server/src/queries/complex/assets/__tests__/assets.spec.ts @@ -95,17 +95,6 @@ describe("getAssetWithVariants", () => { expect(result).toMatchInlineSnapshot(` [ - { - "coinDecimals": 6, - "coinDenom": "USDC", - "coinGeckoId": "usd-coin", - "coinImageUrl": "/tokens/generated/usdc.svg", - "coinMinimalDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", - "coinName": "USDC", - "isUnstable": false, - "isVerified": true, - "variantGroupKey": "USDC", - }, { "coinDecimals": 6, "coinDenom": "USDC.axl", @@ -150,6 +139,17 @@ describe("getAssetWithVariants", () => { "isVerified": true, "variantGroupKey": "USDC", }, + { + "coinDecimals": 6, + "coinDenom": "USDC", + "coinGeckoId": "usd-coin", + "coinImageUrl": "/tokens/generated/usdc.svg", + "coinMinimalDenom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "coinName": "USDC", + "isUnstable": false, + "isVerified": true, + "variantGroupKey": "USDC", + }, { "coinDecimals": 6, "coinDenom": "USDC.wh", diff --git a/packages/server/src/queries/complex/assets/ethereum.ts b/packages/server/src/queries/complex/assets/ethereum.ts new file mode 100644 index 0000000000..6a58174409 --- /dev/null +++ b/packages/server/src/queries/complex/assets/ethereum.ts @@ -0,0 +1,40 @@ +import { + EthereumChainInfo, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; +import { Address, createPublicClient, erc20Abi, http } from "viem"; + +export async function getEvmBalance({ + address, + userAddress, + chainId, +}: { + address: string; + userAddress: string; + chainId: number; +}) { + const evmChain = Object.values(EthereumChainInfo).find( + (chain) => String(chain.id) === String(chainId) + ); + + if (!evmChain) { + throw new Error(`Chain with id ${chainId} not found`); + } + + const publicClient = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); + + const balance = + address === NativeEVMTokenConstantAddress + ? await publicClient.getBalance({ address: userAddress as Address }) + : await publicClient.readContract({ + abi: erc20Abi, + address: address as Address, + functionName: "balanceOf", + args: [userAddress as Address], + }); + + return balance; +} diff --git a/packages/server/src/queries/complex/assets/index.ts b/packages/server/src/queries/complex/assets/index.ts index e14b037445..3b30028b77 100644 --- a/packages/server/src/queries/complex/assets/index.ts +++ b/packages/server/src/queries/complex/assets/index.ts @@ -67,7 +67,7 @@ export function getAssetWithVariants({ return ( variants // place the canonical asset at the beginning - .sort((a) => (a.coinDenom === asset.variantGroupKey ? -1 : 1)) + .sort((a) => (a.coinMinimalDenom === asset.variantGroupKey ? -1 : 1)) ); } @@ -193,6 +193,7 @@ function filterAssetList( export * from "./bridge"; export * from "./categories"; export * from "./config"; +export * from "./ethereum"; export * from "./gas"; export * from "./market"; export * from "./price"; diff --git a/packages/server/src/queries/complex/assets/user.ts b/packages/server/src/queries/complex/assets/user.ts index 85f7cade1b..1c0f54547b 100644 --- a/packages/server/src/queries/complex/assets/user.ts +++ b/packages/server/src/queries/complex/assets/user.ts @@ -31,20 +31,20 @@ export async function getAssetWithUserBalance({ assetLists, chainList, asset, - userOsmoAddress, + userCosmosAddress, }: { assetLists: AssetList[]; chainList: Chain[]; asset: TAsset; - userOsmoAddress?: string; + userCosmosAddress?: string; }): Promise { - if (!userOsmoAddress) return asset; + if (!userCosmosAddress) return asset; const userAssets = await mapGetAssetsWithUserBalances({ assetLists, chainList, assets: [asset], - userOsmoAddress, + userCosmosAddress: userCosmosAddress, includePreview: true, }); return userAssets[0]; @@ -62,14 +62,14 @@ export async function mapGetAssetsWithUserBalances< assetLists: AssetList[]; chainList: Chain[]; assets?: TAsset[]; - userOsmoAddress?: string; + userCosmosAddress?: string; sortFiatValueDirection?: SortDirection; /** * If poolId is provided, only include assets that are part of the pool. */ poolId?: string; } & AssetFilter): Promise<(TAsset & MaybeUserAssetCoin)[]> { - const { userOsmoAddress, search, sortFiatValueDirection } = params; + const { userCosmosAddress, search, sortFiatValueDirection } = params; let { assets } = params; if (!assets) assets = getAssets(params) as TAsset[]; @@ -87,11 +87,11 @@ export async function mapGetAssetsWithUserBalances< ) as TAsset[]; } - if (!userOsmoAddress) return assets; + if (!userCosmosAddress) return assets; const { balances } = await queryBalances({ ...params, - bech32Address: userOsmoAddress, + bech32Address: userCosmosAddress, }); const eventualUserAssets = assets diff --git a/packages/trpc/src/assets.ts b/packages/trpc/src/assets.ts index 7eeca4a4ca..1c91abab70 100644 --- a/packages/trpc/src/assets.ts +++ b/packages/trpc/src/assets.ts @@ -55,7 +55,7 @@ export const assetsRouter = createTRPCRouter({ return await getAssetWithUserBalance({ ...ctx, asset, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, }); } ), @@ -87,7 +87,7 @@ export const assetsRouter = createTRPCRouter({ mapGetAssetsWithUserBalances({ ...ctx, search, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, onlyVerified, sortFiatValueDirection: "desc", includePreview, @@ -165,7 +165,7 @@ export const assetsRouter = createTRPCRouter({ const userAsset = await getAssetWithUserBalance({ ...ctx, asset, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, }); const userMarketAsset = await getMarketAsset({ asset: userAsset, @@ -290,7 +290,7 @@ export const assetsRouter = createTRPCRouter({ ...ctx, search, categories, - userOsmoAddress, + userCosmosAddress: userOsmoAddress, includePreview, }); @@ -543,7 +543,7 @@ export const assetsRouter = createTRPCRouter({ ...ctx, search, // Only get balances for withdraw - userOsmoAddress: + userCosmosAddress: type === "withdraw" ? userOsmoAddress : undefined, sortFiatValueDirection: "desc", includePreview, @@ -571,7 +571,7 @@ export const assetsRouter = createTRPCRouter({ asset.variantGroupKey as (typeof variantsNotToBeExcluded)[number] ) ) { - return asset.variantGroupKey === asset.coinDenom; + return asset.variantGroupKey === asset.coinMinimalDenom; } return true; diff --git a/packages/trpc/src/chains.ts b/packages/trpc/src/chains.ts index 01905780d3..7392e9aaf7 100644 --- a/packages/trpc/src/chains.ts +++ b/packages/trpc/src/chains.ts @@ -1,8 +1,4 @@ -import { - CursorPaginationSchema, - getChain, - maybeCachePaginatedItems, -} from "@osmosis-labs/server"; +import { getChain } from "@osmosis-labs/server"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; @@ -20,24 +16,4 @@ export const chainsRouter = createTRPCRouter({ chainNameOrId: findChainNameOrId, }) ), - getChains: publicProcedure - .input( - CursorPaginationSchema.merge(z.object({ search: z.string().optional() })) - ) - .query(async ({ input: { cursor, limit, search }, ctx }) => - maybeCachePaginatedItems({ - cacheKey: "chains", - getFreshItems: () => - Promise.resolve( - ctx.chainList.filter((chain) => { - return search - ? chain.chain_name.includes(search) || - chain.pretty_name.includes(search) - : true; - }) - ), - cursor, - limit, - }) - ), }); diff --git a/packages/trpc/src/parameter-types.ts b/packages/trpc/src/parameter-types.ts index 7027ceaa24..9b52702e10 100644 --- a/packages/trpc/src/parameter-types.ts +++ b/packages/trpc/src/parameter-types.ts @@ -7,3 +7,13 @@ export type UserOsmoAddress = z.infer; export const UserOsmoAddressSchema = z.object({ userOsmoAddress: z.string().startsWith("osmo").optional(), }); + +export type UserCosmosAddress = z.infer; +export const UserCosmosAddressSchema = z.object({ + userCosmosAddress: z.string().optional(), +}); + +export type UserEvmAddress = z.infer; +export const UserEvmAddressSchema = z.object({ + userEvmAddress: z.string().startsWith("0x").optional(), +}); diff --git a/packages/types/src/chain-types.ts b/packages/types/src/chain-types.ts index 067e3061e7..c4dafdd40d 100644 --- a/packages/types/src/chain-types.ts +++ b/packages/types/src/chain-types.ts @@ -18,6 +18,16 @@ export interface Chain { bech32_config: Bech32Config; slip44: number; alternative_slip44s?: number[]; + logoURIs?: { + png?: string; + svg?: string; + layout?: "logomark"; + theme?: { + primary_color_hex?: string; + dark_mode?: false; + circle?: true; + }; + }; fees: { fee_tokens: FeeToken[]; }; diff --git a/packages/utils/package.json b/packages/utils/package.json index 2aead4587f..f8f3cc0b07 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -16,7 +16,8 @@ "dependencies": { "@keplr-wallet/unit": "0.10.24-ibc.go.v7.hot.fix", "@osmosis-labs/types": "^1.0.0", - "sha.js": "^2.4.11" + "sha.js": "^2.4.11", + "viem": "2.16.4" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/bridge/src/ethereum.ts b/packages/utils/src/ethereum.ts similarity index 81% rename from packages/bridge/src/ethereum.ts rename to packages/utils/src/ethereum.ts index f37522ace7..a26d16a124 100644 --- a/packages/bridge/src/ethereum.ts +++ b/packages/utils/src/ethereum.ts @@ -17,15 +17,45 @@ import { polygonMumbai, } from "viem/chains"; -import { SourceChain } from "./chain"; +/** + * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, + * to be handled similarly to other ERC20 tokens. + */ +export const NativeEVMTokenConstantAddress = + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + +/** Human-displayable global source chain identifiers. + * TODO: use global chain IDs instead of display names as keys + * + * @deprecated + */ +export type AxelarSourceChain = + | "Bitcoin" + | "Bitcoin Testnet" + | "Aurora Testnet" + | "Avalanche" + | "Avalanche Fuji Testnet" + | "Binance Smart Chain" + | "BSC Testnet" + | "Ethereum" + | "Goerli Testnet" + | "Fantom" + | "Fantom Testnet" + | "Moonbeam" + | "Moonbase Alpha" + | "Polygon" + | "Mumbai" + | "Filecoin" + | "Filecoin Hyperspace" + | "Arbitrum"; -// TODO maybe we can use EVM chain ID (numeric) or ethereum chain registry +// TODO: maybe we can use EVM chain ID (numeric) or ethereum chain registry const createEthereumChainInfo = < Dict extends Partial< Record< - SourceChain, + AxelarSourceChain, Chain & { - chainName: SourceChain; + chainName: AxelarSourceChain; clientChainId: string; } > @@ -40,7 +70,7 @@ const mapChainInfo = ({ clientChainId, }: { chain: Chain; - axelarChainName: SourceChain; + axelarChainName: AxelarSourceChain; clientChainId: string; }) => ({ ...chain, @@ -125,10 +155,3 @@ export const EthereumChainInfo = createEthereumChainInfo({ clientChainId: "Filecoin Hyperspace", }), }); - -/** - * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, - * to be handled similarly to other ERC20 tokens. - */ -export const NativeEVMTokenConstantAddress = - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 68ead12a25..99ff9306d9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,6 +9,7 @@ export * from "./coin"; export * from "./common-utils"; export * from "./compare"; export * from "./date"; +export * from "./ethereum"; export * from "./function"; export * from "./gas-utils"; export * from "./ibc-utils"; diff --git a/packages/web/__tests__/index-page.spec.tsx b/packages/web/__tests__/index-page.spec.tsx index 3ab1202ec7..7d67760b24 100644 --- a/packages/web/__tests__/index-page.spec.tsx +++ b/packages/web/__tests__/index-page.spec.tsx @@ -13,6 +13,16 @@ import HomePage, { PreviousTrade, SwapPreviousTradeKey } from "~/pages"; jest.mock("next/router", () => jest.requireActual("next-router-mock")); +// Mock the ResizeObserver +const ResizeObserverMock = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Stub the global ResizeObserver +global.ResizeObserver = ResizeObserverMock; + const atomAsset = getAssetFromAssetList({ assetLists: AssetLists, sourceDenom: "uatom", @@ -38,7 +48,10 @@ afterEach(() => { beforeEach(() => { server.use( trpcMsw.edge.assets.getUserAssets.query((_req, res, ctx) => { - return res(ctx.status(200), ctx.data({ items: [], nextCursor: null })); + return res( + ctx.status(200), + ctx.data({ items: [], nextCursor: undefined }) + ); }), trpcMsw.edge.assets.getAssetPrice.query((_req, res, ctx) => { return res( diff --git a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx new file mode 100644 index 0000000000..49e1ede849 --- /dev/null +++ b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx @@ -0,0 +1,126 @@ +import { CoinPretty } from "@keplr-wallet/unit"; +import { BridgeChain } from "@osmosis-labs/bridge"; +import { MinimalAsset } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; + +import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { ImmersiveBridgeScreens } from "~/components/bridge/immersive/immersive-bridge"; +import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; +import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; +import { Screen } from "~/components/screen-manager"; +import { Button } from "~/components/ui/button"; +import { useEvmWalletAccount } from "~/hooks/evm-wallet"; +import { useStore } from "~/stores"; + +export type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; + +export type SupportedAssetWithAmount = SupportedAsset & { amount: CoinPretty }; + +interface AmountAndConfirmationScreenProps { + direction: "deposit" | "withdraw"; + selectedAssetDenom: string | undefined; + onClose: () => void; +} + +export const AmountAndConfirmationScreen = observer( + ({ + direction, + selectedAssetDenom, + onClose, + }: AmountAndConfirmationScreenProps) => { + const { accountStore } = useStore(); + + const [sourceAsset, setSourceAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); + const [fromChain, setFromChain] = useState(); + const [toChain, setToChain] = useState(); + + const [cryptoAmount, setCryptoAmount] = useState("0"); + const [fiatAmount, setFiatAmount] = useState("0"); + + // Wallets + const destinationAccount = accountStore.getWallet( + accountStore.osmosisChainId + ); + const { address: evmAddress } = useEvmWalletAccount(); + + const sourceChain = direction === "deposit" ? fromChain : toChain; + const destinationChain = direction === "deposit" ? toChain : fromChain; + + const cosmosCounterpartyAccount = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWallet(sourceChain.chainId); + + const sourceAddress = + sourceChain?.chainType === "evm" + ? evmAddress + : cosmosCounterpartyAccount?.address; + + const quote = useBridgeQuote({ + destinationAddress: destinationAccount?.address, + destinationChain, + destinationAsset: destinationAsset + ? { + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + denom: destinationAsset.coinDenom, + } + : undefined, + sourceAddress, + sourceChain, + sourceAsset, + direction, + onRequestClose: onClose, + inputAmount: cryptoAmount, + bridges: sourceAsset?.supportedProviders, + onTransfer: () => { + setCryptoAmount("0"); + setFiatAmount("0"); + }, + }); + + if (!selectedAssetDenom) return; + + return ( + <> + + {() => ( + + )} + + + {({ goBack }) => ( +
+
Step 3: Review
+ + +
+ )} +
+ + ); + } +); diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index bb79ba32b7..a26163fa9d 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -1,42 +1,131 @@ -import { Menu } from "@headlessui/react"; +import { + Disclosure, + DisclosureButton, + DisclosurePanel, + Menu, + MenuButton, + MenuItem, + MenuItems, +} from "@headlessui/react"; import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; +import { BridgeChain } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; -import { MinimalAsset } from "@osmosis-labs/types"; -import { isNumeric, noop } from "@osmosis-labs/utils"; +import { BridgeTransactionDirection, MinimalAsset } from "@osmosis-labs/types"; +import { isNil, isNumeric, noop } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { FunctionComponent, ReactNode, useState } from "react"; +import { + FunctionComponent, + ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import { Icon } from "~/components/assets"; +import { SupportedAssetWithAmount } from "~/components/bridge/immersive/amount-and-confirmation-screen"; import { BridgeNetworkSelectModal } from "~/components/bridge/immersive/bridge-network-select-modal"; +import { BridgeProviderDropdown } from "~/components/bridge/immersive/bridge-provider-dropdown"; +import { BridgeQuoteRemainingTime } from "~/components/bridge/immersive/bridge-quote-remaining-time"; +import { BridgeWalletSelectModal } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { MoreBridgeOptions } from "~/components/bridge/immersive/more-bridge-options"; +import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; +import { useBridgesSupportedAssets } from "~/components/bridge/immersive/use-bridges-supported-assets"; import { InputBox } from "~/components/input"; import { SkeletonLoader, Spinner } from "~/components/loaders"; import { Tooltip } from "~/components/tooltip"; import { Button } from "~/components/ui/button"; -import { useConnectWalletModalRedirect, useTranslation } from "~/hooks"; +import { + useConnectWalletModalRedirect, + useDisclosure, + useTranslation, + useWalletSelect, +} from "~/hooks"; +import { useEvmWalletAccount } from "~/hooks/evm-wallet"; import { usePrice } from "~/hooks/queries/assets/use-price"; import { useStore } from "~/stores"; import { trimPlaceholderZeros } from "~/utils/number"; import { api } from "~/utils/trpc"; +type SupportedAsset = ReturnType< + typeof useBridgesSupportedAssets +>["supportedAssetsByChainId"][string][number]; + interface AmountScreenProps { - type: "deposit" | "withdraw"; + direction: "deposit" | "withdraw"; + selectedDenom: string; /** - * Includes both the canonical asset and its variants. + * Chain taking into account the direction. */ - assetsInOsmosis: MinimalAsset[] | undefined; + sourceChain: BridgeChain | undefined; + destinationChain: BridgeChain | undefined; + + fromChain: BridgeChain | undefined; + setFromChain: (chain: BridgeChain) => void; + toChain: BridgeChain | undefined; + setToChain: (chain: BridgeChain) => void; + + sourceAsset: SupportedAssetWithAmount | undefined; + setSourceAsset: (asset: SupportedAssetWithAmount | undefined) => void; + destinationAsset: MinimalAsset | undefined; + setDestinationAsset: (asset: MinimalAsset | undefined) => void; + + cryptoAmount: string; + fiatAmount: string; + setCryptoAmount: (amount: string) => void; + setFiatAmount: (amount: string) => void; + + quote: ReturnType; } export const AmountScreen = observer( - ({ type, assetsInOsmosis }: AmountScreenProps) => { + ({ + direction, + selectedDenom, + + sourceChain, + + fromChain, + setFromChain, + toChain, + setToChain, + + sourceAsset, + setSourceAsset, + + destinationAsset, + setDestinationAsset, + + cryptoAmount, + setCryptoAmount, + fiatAmount, + setFiatAmount, + + quote, + }: AmountScreenProps) => { const { accountStore } = useStore(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); - const [isMoreOptionsVisible, setIsMoreOptionsVisible] = useState(false); + const { onOpenWalletSelect } = useWalletSelect(); const { t } = useTranslation(); + const { + selectedQuote, + successfulQuotes, + setSelectedBridgeProvider, + buttonErrorMessage, + buttonText, + isLoadingBridgeQuote, + isLoadingBridgeTransaction, + isRefetchingQuote, + selectedQuoteUpdatedAt, + refetchInterval, + isInsufficientBal, + isInsufficientFee, + warnUserOfPriceImpact, + warnUserOfSlippage, + } = quote; + const { accountActionButton: connectWalletButton, walletConnected } = useConnectWalletModalRedirect( { @@ -45,37 +134,325 @@ export const AmountScreen = observer( noop ); + const [areMoreOptionsVisible, setAreMoreOptionsVisible] = useState(false); + + const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); + const { + isOpen: isBridgeWalletSelectOpen, + onClose: onCloseBridgeWalletSelect, + onOpen: onOpenBridgeWalletSelect, + } = useDisclosure(); + + // Wallets + const destinationAccount = accountStore.getWallet( + accountStore.osmosisChainId + ); + const { + address: evmAddress, + connector: evmConnector, + isConnected: isEvmWalletConnected, + } = useEvmWalletAccount(); + + const cosmosCounterpartyAccountRepo = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWalletRepo(sourceChain.chainId); + const cosmosCounterpartyAccount = + sourceChain?.chainType === "evm" || isNil(sourceChain) + ? undefined + : accountStore.getWallet(sourceChain.chainId); + + const sourceAddress = + sourceChain?.chainType === "evm" + ? evmAddress + : cosmosCounterpartyAccount?.address; + + const { data: assetsInOsmosis } = + api.edge.assets.getCanonicalAssetWithVariants.useQuery( + { + findMinDenomOrSymbol: selectedDenom!, + }, + { + enabled: !isNil(selectedDenom), + cacheTime: 10 * 60 * 1000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes + } + ); + const { data: osmosisChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "osmosis", - }); - const { data: nobleChain } = api.edge.chains.getChain.useQuery({ - findChainNameOrId: "noble", + findChainNameOrId: accountStore.osmosisChainId, }); const canonicalAsset = assetsInOsmosis?.[0]; - const { price: assetInOsmosisPrice, isLoading } = usePrice(canonicalAsset); - const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); - const [cryptoAmount, setCryptoAmount] = useState("0"); - const [fiatAmount, setFiatAmount] = useState("0"); + const { + price: assetInOsmosisPrice, + isLoading: isLoadingCanonicalAssetPrice, + } = usePrice( + /** + * Use the canonical osmosis asset to determine the price of the assets. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + canonicalAsset + ); + + const { supportedAssetsByChainId, supportedChains } = + useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); + + const supportedAssets = + supportedAssetsByChainId[sourceChain?.chainId ?? ""]; + + const supportedChainsAsBridgeChain = useMemo( + () => + supportedChains.map( + ({ chainId, chainType, prettyName }) => + ({ + chainId, + chainType, + chainName: prettyName, + } as BridgeChain) + ), + [supportedChains] + ); + + const firstSupportedEvmChain = useMemo( + () => + supportedChainsAsBridgeChain.find( + (chain): chain is Extract => + chain.chainType === "evm" + ), + [supportedChainsAsBridgeChain] + ); + const firstSupportedCosmosChain = useMemo( + () => + supportedChainsAsBridgeChain.find( + (chain): chain is Extract => + chain.chainType === "cosmos" + ), + [supportedChainsAsBridgeChain] + ); + + const hasMoreThanOneChainType = + !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); + + const { + data: sourceAssetsBalances, + isLoading: isLoadingSourceAssetsBalance, + } = api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( + sourceChain?.chainType === "evm" + ? { + type: "evm", + assets: supportedAssets as Extract< + SupportedAsset, + { chainType: "evm" } + >[], + userEvmAddress: evmAddress, + } + : { + type: "cosmos", + assets: supportedAssets as Extract< + SupportedAsset, + { chainType: "cosmos" } + >[], + userCosmosAddress: cosmosCounterpartyAccount?.address, + }, + { + enabled: !isNil(sourceChain) && !isNil(supportedAssets), + + select: (data) => { + let nextData: typeof data = data; + + // Filter out assets with no balance + if (nextData) { + const filteredData = nextData.filter((asset) => + asset.amount.toDec().gt(new Dec(0)) + ); + + // If there are no assets with balance, leave one to be selected + if (filteredData.length === 0) { + nextData = [nextData[0]]; + } else { + nextData = filteredData; + } + } + + if (!sourceAsset && nextData) { + const highestBalance = nextData.reduce( + (acc, curr) => + curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, + nextData[0] + ); + + setSourceAsset(highestBalance); + } + + return nextData; + }, + } + ); + + /** + * Set the initial destination asset based on the source asset. + */ + useEffect(() => { + if (!isNil(sourceAsset) && !isNil(assetsInOsmosis)) { + const destinationAsset = assetsInOsmosis.find( + (a) => a.coinMinimalDenom === sourceAsset.supportedVariants[0] + )!; + + setDestinationAsset(destinationAsset); + } + }, [assetsInOsmosis, setDestinationAsset, sourceAsset]); + + /** + * Set the osmosis chain based on the direction + */ + useEffect(() => { + const chain = direction === "deposit" ? toChain : fromChain; + const setChain = direction === "deposit" ? setToChain : setFromChain; + if (isNil(chain) && !isNil(osmosisChain)) { + setChain({ + chainId: osmosisChain.chain_id, + chainName: osmosisChain.pretty_name, + chainType: "cosmos", + }); + } + }, [direction, fromChain, osmosisChain, setFromChain, setToChain, toChain]); + + /** + * Set the initial chain based on the direction. + * TODO: When all balances are computed per chain, we can default to the highest balance + * instead of the first one. + */ + useEffect(() => { + const chain = direction === "deposit" ? fromChain : toChain; + const setChain = direction === "deposit" ? setFromChain : setToChain; + if ( + isNil(chain) && + !isNil(supportedChains) && + supportedChains.length > 0 + ) { + const firstChain = supportedChains[0]; + setChain({ + chainId: firstChain.chainId, + chainName: firstChain.prettyName, + chainType: firstChain.chainType, + } as BridgeChain); + } + }, [ + direction, + fromChain, + setFromChain, + setToChain, + supportedChains, + toChain, + ]); + + /** + * Connect cosmos wallet to the counterparty chain + */ + useEffect(() => { + if (!fromChain || !toChain) return; + + const chain = direction === "deposit" ? fromChain : toChain; + + if ( + // If the chain is an EVM chain, we don't need to connect the cosmos chain + chain.chainType !== "cosmos" || + // Or if the account is already connected + !!cosmosCounterpartyAccount?.address || + // Or if there's no available cosmos chain + !firstSupportedCosmosChain || + // Or if the account is already connected + !!cosmosCounterpartyAccountRepo?.current + ) { + return; + } + + cosmosCounterpartyAccountRepo + ?.connect(destinationAccount?.walletName) + .catch(() => + // Display the connect modal if the user for some reason rejects the connection + onOpenWalletSelect({ + walletOptions: [ + { walletType: "cosmos", chainId: String(chain.chainId) }, + ], + }) + ); + }, [ + destinationAccount?.walletName, + cosmosCounterpartyAccount?.address, + cosmosCounterpartyAccountRepo, + direction, + firstSupportedCosmosChain, + fromChain, + onOpenWalletSelect, + toChain, + ]); + + /** + * Connect evm wallet to the counterparty chain + */ + useEffect(() => { + if (!fromChain || !toChain) return; + + const chain = direction === "deposit" ? fromChain : toChain; + + if ( + // If the chain is an Cosmos chain, we don't need to connect the cosmos chain + chain.chainType !== "evm" || + // Or if the account is already connected + isEvmWalletConnected || + // Or if there's no available evm chain + !firstSupportedEvmChain + ) { + return; + } + + onOpenBridgeWalletSelect(); + }, [ + direction, + evmAddress, + firstSupportedEvmChain, + fromChain, + isEvmWalletConnected, + onOpenBridgeWalletSelect, + toChain, + ]); if ( - isLoading || + isLoadingCanonicalAssetPrice || + isNil(supportedAssets) || !assetsInOsmosis || !canonicalAsset || + !destinationAsset || !assetInOsmosisPrice || - !osmosisChain || - !nobleChain + !fromChain || + !toChain || + !sourceAsset ) { return ; } const cryptoAmountPretty = new CoinPretty( - canonicalAsset, + { + coinDecimals: sourceAsset.decimals, + coinDenom: sourceAsset.denom, + coinMinimalDenom: sourceAsset.address, + }, cryptoAmount === "" ? new Dec(0) : new Dec(cryptoAmount).mul( - DecUtils.getTenExponentN(canonicalAsset.coinDecimals) + DecUtils.getTenExponentN(sourceAsset.decimals) ) ); @@ -125,11 +502,22 @@ export const AmountScreen = observer( type === "fiat" ? setFiatAmount(nextValue) : setCryptoAmount(nextValue); }; + const resetAssets = () => { + setSourceAsset(undefined); + setDestinationAsset(undefined); + }; + + const dropdownActiveItemIcon = ( +
+ +
+ ); + return (
- {type === "deposit" + {direction === "deposit" ? t("transfer.deposit") : t("transfer.withdraw")} {" "} @@ -154,14 +542,32 @@ export const AmountScreen = observer(
- - {nobleChain.pretty_name} + { + setFromChain(nextChain); + resetAssets(); + }} + readonly={direction === "withdraw"} + > + {fromChain.chainName} - - {osmosisChain.pretty_name} + { + setToChain(nextChain); + resetAssets(); + }} + readonly={direction === "deposit"} + > + {toChain.chainName}
@@ -175,7 +581,12 @@ export const AmountScreen = observer( @@ -189,7 +600,10 @@ export const AmountScreen = observer( onInput={onInput("crypto")} className="w-full border-none bg-transparent text-center" classes={{ - input: "px-0", + input: classNames("px-0", { + "text-rust-300": + isInsufficientBal || isInsufficientFee, + }), trailingSymbol: "ml-1 align-middle text-2xl text-osmoverse-500 text-left absolute right-0", }} @@ -234,179 +648,552 @@ export const AmountScreen = observer( -
- {[ - { - label: "USDC.e", - amount: `$80.00 ${t("transfer.available")}`, - active: true, - }, - { label: "USDC", amount: "$30.00", active: false }, - { label: "USDC.axl", amount: "$10.00", active: false }, - ].map(({ label, amount, active }, index) => ( - - ))} -
- -
- - {type === "deposit" - ? t("transfer.transferWith") - : t("transfer.transferTo")} - -
- {wallet?.walletInfo.prettyName} - {wallet?.walletInfo.prettyName} - -
-
+ <> + {isLoadingSourceAssetsBalance && ( +
+ +

Looking for balances

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

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

-

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

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

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

+
+ )} -
- USDC - 1 && ( +
+ {(sourceAssetsBalances ?? []).map((asset) => { + const isActive = + asset.amount.currency.coinMinimalDenom === + sourceAsset?.address; + return ( +
-
-
- - - - + ); + })} +
+ )} + + + {walletConnected && ( + <> + {hasMoreThanOneChainType ? ( + <> + + + { + const setChain = + direction === "deposit" ? setFromChain : setToChain; + setChain(chain); + resetAssets(); + }} + evmChain={ + sourceChain?.chainType === "evm" + ? sourceChain + : firstSupportedEvmChain + } + cosmosChain={ + sourceChain?.chainType === "cosmos" + ? sourceChain + : firstSupportedCosmosChain + } + /> + + ) : ( +
+ + {direction === "deposit" + ? t("transfer.transferWith") + : t("transfer.transferTo")} + + +
+ )} + + )} + + {!isNil(sourceAsset) && sourceAsset.supportedVariants.length > 1 && ( + + {({ open }) => ( +
+ +
+
+ + {t("transfer.receiveAsset")} + + +

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

+

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

+
+ } + > + +
-
+ +
+ + {destinationAsset?.coinDenom} +
- - - - - - -
- )} -
+ + -
-
- + + {sourceAsset.supportedVariants.map( + (variantCoinMinimalDenom, index) => { + const asset = assetsInOsmosis.find( + (asset) => + asset.coinMinimalDenom === variantCoinMinimalDenom + )!; + + const onClick = () => { + setDestinationAsset(asset); + }; + + // Show all as 'deposit as' for now + const isConvert = + false ?? + asset.coinMinimalDenom === asset.variantGroupKey; + const isSelected = + destinationAsset?.coinDenom === asset.coinDenom; + + const isCanonicalAsset = index === 0; + + return ( + + <> + {isCanonicalAsset ? ( + + ) : ( + + )} + + + ); + } + )} + +
+ )} +
+ )} + + {isLoadingBridgeQuote && ( +
+
+ +

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

+
- {t("transfer.estimatingTime")} + {t("transfer.calculatingFees")}
+ )} + {!isLoadingBridgeQuote && !isNil(selectedQuote) && ( + + {({ open }) => ( + <> + {" "} + +
+ {open ? ( +

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

+ ) : ( +

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

+ )} +
+ {!isNil(selectedQuoteUpdatedAt) && ( + + } + /> + )} +
+ {!open && ( + + ~ + {( + selectedQuote.transferFeeFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + ?.add(selectedQuote.gasCost ?? new Dec(0)) + .toString()}{" "} + {t("transfer.fees")} + + )} + {(warnUserOfPriceImpact || warnUserOfSlippage) && ( + + + + )} + +
+
+
+
+ + + setSelectedBridgeProvider(bridgeId) + } + /> + } + isLoading={isRefetchingQuote} + /> + + {" "} +

+ {selectedQuote.estimatedTime.humanize()} +

+ + } + isLoading={isRefetchingQuote} + /> + + {selectedQuote.transferFee + .toDec() + .equals(new Dec(0)) ? ( +

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

+ ) : ( +

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

+ )} + + } + isLoading={isRefetchingQuote} + /> - - {t("transfer.calculatingFees")} - - + + {isNil(selectedQuote.gasCostFiat) && + isNil(selectedQuote.gasCost) ? ( + +
+ +

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

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

+ } + isLoading={isRefetchingQuote} + /> + + {(selectedQuote.gasCostFiat || + selectedQuote.transferFeeFiat) && ( + + {( + selectedQuote?.gasCostFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + .add( + selectedQuote.transferFeeFiat ?? + new PricePretty( + DEFAULT_VS_CURRENCY, + new Dec(0) + ) + ) + .toString()} +

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

+ } + isLoading={isRefetchingQuote} + /> +
+ + )} +
+ )}
{!walletConnected ? ( connectWalletButton ) : ( <> - setIsMoreOptionsVisible(false)} + direction={direction} + isOpen={areMoreOptionsVisible} + fromAsset={sourceAsset} + toAsset={{ + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + denom: destinationAsset.coinDenom, + }} + fromChain={fromChain} + toChain={toChain} + toAddress={sourceAddress} + onRequestClose={() => setAreMoreOptionsVisible(false)} /> )} @@ -418,10 +1205,20 @@ export const AmountScreen = observer( ); const ChainSelectorButton: FunctionComponent<{ - readonly?: boolean; + direction: BridgeTransactionDirection; + readonly: boolean; children: ReactNode; chainLogo: string; -}> = ({ readonly, children, chainLogo: _chainLogo }) => { + chains: ReturnType["supportedChains"]; + onSelectChain: (chain: BridgeChain) => void; +}> = ({ + direction, + readonly, + children, + chainLogo: _chainLogo, + chains, + onSelectChain, +}) => { const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); if (readonly) { @@ -448,10 +1245,18 @@ const ChainSelectorButton: FunctionComponent<{ height={12} /> - setIsNetworkSelectVisible(false)} - /> + {!isNil(chains) && !isNil(onSelectChain) && ( + { + onSelectChain(chain); + setIsNetworkSelectVisible(false); + }} + onRequestClose={() => setIsNetworkSelectVisible(false)} + direction={direction} + /> + )} ); }; @@ -470,3 +1275,37 @@ const AmountScreenSkeletonLoader = () => {
); }; + +const WalletDisplay: FunctionComponent<{ + icon: string | undefined; + name: string | undefined; + suffix?: ReactNode; +}> = ({ icon, name, suffix }) => { + return ( +
+ {!isNil(icon) && {name}} + {name} + {suffix} +
+ ); +}; + +const TransferDetailRow: FunctionComponent<{ + label: string; + value: ReactNode; + isLoading: boolean; +}> = ({ label, value, isLoading }) => { + return ( +
+

{label}

+ + {value} + +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/asset-select-screen.tsx b/packages/web/components/bridge/immersive/asset-select-screen.tsx index 02bfebdb4c..8ee2cdb08f 100644 --- a/packages/web/components/bridge/immersive/asset-select-screen.tsx +++ b/packages/web/components/bridge/immersive/asset-select-screen.tsx @@ -136,9 +136,9 @@ export const AssetSelectScreen = observer(
{isLoading ? ( - <> +
- +
) : ( <> {assets.map((asset) => ( diff --git a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index f2f5a4e8c2..76967e323e 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -1,89 +1,182 @@ -import { debounce } from "debounce"; +import type { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import classNames from "classnames"; import React, { useMemo, useState } from "react"; +import { BridgeWalletSelectScreen } from "~/components/bridge/immersive/bridge-wallet-select-modal"; import { SearchBox } from "~/components/input"; -import { Intersection } from "~/components/intersection"; -import { SkeletonLoader, Spinner } from "~/components/loaders"; +import { + Screen, + ScreenGoBackButton, + ScreenManager, +} from "~/components/screen-manager"; +import { SwitchingNetworkState } from "~/components/wallet-states/switching-network-state"; +import { EthereumChainIds } from "~/config/wagmi"; +import { useEvmWalletAccount, useSwitchEvmChain } from "~/hooks/evm-wallet"; import { useTranslation } from "~/hooks/language"; import { ModalBase, ModalBaseProps } from "~/modals"; -import { api } from "~/utils/trpc"; -export const BridgeNetworkSelectModal = (modalProps: ModalBaseProps) => { +enum Screens { + Main = "main", + SelectWallet = "select-wallet", +} +interface BridgeNetworkSelectModalProps extends ModalBaseProps { + direction: BridgeTransactionDirection; + chains: { + prettyName: string; + chainId: BridgeChain["chainId"]; + chainType: BridgeChain["chainType"]; + }[]; + onSelectChain: (chain: BridgeChain) => void; +} + +export const BridgeNetworkSelectModal = ({ + direction, + chains, + onSelectChain, + ...modalProps +}: BridgeNetworkSelectModalProps) => { const { t } = useTranslation(); + const [isSwitchingChain, setIsSwitchingChain] = useState(false); + + const [connectingToEvmChain, setConnectingToEvmChain] = + useState>(); + + const { + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + connector, + } = useEvmWalletAccount(); + const { switchChainAsync } = useSwitchEvmChain(); + const [query, setQuery] = useState(""); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - api.edge.chains.getChains.useInfiniteQuery( - { - limit: 50, - search: query, - }, - { - enabled: modalProps.isOpen, - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialCursor: 0, - keepPreviousData: true, - trpc: { - context: { - skipBatch: true, - }, - }, - } + const filteredChains = useMemo(() => { + return chains.filter(({ prettyName }) => + prettyName.toLowerCase().includes(query.toLowerCase()) ); - - const chains = useMemo( - () => data?.pages.flatMap((page) => page?.items) ?? [], - [data] - ); - const canLoadMore = !isLoading && !isFetchingNextPage && hasNextPage; + }, [chains, query]); return ( - - { - setQuery(nextValue); - }, 300)} - className="my-4 flex-shrink-0" - placeholder={t("transfer.bridgeNetworkSelect.searchPlaceholder")} - size="full" - /> -
- {isLoading ? ( - <> - {new Array(3).fill(undefined).map((_, i) => ( - - ))} - - ) : ( - <> - {chains.map(({ chain_name, pretty_name }) => ( - - ))} - { - if (canLoadMore) { - fetchNextPage(); - } - }} - /> - {isFetchingNextPage && ( -
- + + {({ currentScreen, setCurrentScreen }) => ( + <> + { + setQuery(""); + setCurrentScreen(Screens.Main); + }} + > + +
+ { + setConnectingToEvmChain(undefined); + }} + /> + { + modalProps.onRequestClose(); + }} + direction={direction} + onSelectChain={(chain) => { + onSelectChain(chain); + }} + evmChain={connectingToEvmChain} + /> +
+
+ + +
+ {isEvmWalletConnected && isSwitchingChain && ( +
+ +
+ )} + +
+ { + setQuery(nextValue); + }} + className="my-4 flex-shrink-0" + placeholder={t( + "transfer.bridgeNetworkSelect.searchPlaceholder" + )} + size="full" + /> +
+ {filteredChains.map((chain) => ( + + ))} +
+
- )} - - )} -
- + + + + )} + ); }; diff --git a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx new file mode 100644 index 0000000000..072cdbc84e --- /dev/null +++ b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx @@ -0,0 +1,143 @@ +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; +import { Dec, PricePretty } from "@keplr-wallet/unit"; +import { Bridge } from "@osmosis-labs/bridge"; +import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import classNames from "classnames"; +import Image from "next/image"; +import { useMemo } from "react"; + +import { Icon } from "~/components/assets"; +import { useBridgeQuote } from "~/components/bridge/immersive/use-bridge-quote"; +import { useTranslation } from "~/hooks"; + +interface Props { + selectedQuote: NonNullable< + ReturnType["selectedQuote"] + >; + quotes: ReturnType["successfulQuotes"]; + onSelect: (bridge: Bridge) => void; +} + +export const BridgeProviderDropdown = ({ + selectedQuote, + quotes, + onSelect, +}: Props) => { + const { t } = useTranslation(); + const fastestQuote = useMemo( + () => + quotes.reduce((prev, current) => + prev.data.estimatedTime.asMilliseconds() < + current.data.estimatedTime.asMilliseconds() + ? prev + : current + ), + [quotes] + ); + return ( + + {({ open }) => ( +
+ + {`${selectedQuote.provider.id} + {selectedQuote.provider.id}{" "} + + + + {quotes.map( + ( + { + data: { + provider, + estimatedTime, + transferFeeFiat, + gasCostFiat, + expectedOutputFiat, + }, + }, + index + ) => { + const totalFee = transferFeeFiat + ?.add( + gasCostFiat ?? + new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)) + ) + .toString(); + const isSelected = selectedQuote.provider.id === provider.id; + return ( + + + + ); + } + )} + +
+ )} +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/bridge-quote-remaining-time.tsx b/packages/web/components/bridge/immersive/bridge-quote-remaining-time.tsx new file mode 100644 index 0000000000..183ce051cf --- /dev/null +++ b/packages/web/components/bridge/immersive/bridge-quote-remaining-time.tsx @@ -0,0 +1,77 @@ +import classNames from "classnames"; +import { FunctionComponent, useEffect, useState } from "react"; + +export const BridgeQuoteRemainingTime: FunctionComponent<{ + className?: string; + refetchInterval: number; + expiredElement?: React.ReactNode; + dataUpdatedAt: number; +}> = ({ className, refetchInterval, expiredElement, dataUpdatedAt }) => { + const [progress, setProgress] = useState(100); + + useEffect(() => { + if (!dataUpdatedAt) return; + + const updateProgress = () => { + const elapsed = Date.now() - dataUpdatedAt; + const percentage = Math.max((1 - elapsed / refetchInterval) * 100, 0); + setProgress(percentage); + }; + + updateProgress(); + + const intervalId = setInterval( + () => { + updateProgress(); + }, + 1000 // Update every ms + ); + + return () => clearInterval(intervalId); + }, [dataUpdatedAt, refetchInterval]); + + if (progress <= 0) { + return expiredElement; + } + + return ( +
+
+ +
+
+ ); +}; + +const RadialProgress: FunctionComponent<{ progress: number }> = ({ + progress, +}) => { + const radius = 20; + const circumference = 2 * Math.PI * radius; + const offset = (progress / 100) * circumference; + return ( + + + + + ); +}; diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx new file mode 100644 index 0000000000..fa9856d5e8 --- /dev/null +++ b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx @@ -0,0 +1,273 @@ +import { BridgeChain } from "@osmosis-labs/bridge"; +import { BridgeTransactionDirection } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import React, { ReactNode, useState } from "react"; +import { Connector } from "wagmi"; + +import { SearchBox } from "~/components/input"; +import { Button } from "~/components/ui/button"; +import { EthereumChainIds } from "~/config/wagmi"; +import { + useDisconnectEvmWallet, + useEvmWalletAccount, +} from "~/hooks/evm-wallet"; +import { ModalBase, ModalBaseProps } from "~/modals"; +import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; +import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; +import { useStore } from "~/stores"; + +interface BridgeWalletSelectProps extends ModalBaseProps { + direction: BridgeTransactionDirection; + cosmosChain?: Extract; + evmChain?: Extract; + onSelectChain: (chain: BridgeChain) => void; +} + +export const BridgeWalletSelectModal = observer( + (props: BridgeWalletSelectProps) => { + const { direction, cosmosChain, evmChain, onSelectChain, ...modalProps } = + props; + + return ( + + + + ); + } +); + +export const BridgeWalletSelectScreen = ({ + cosmosChain, + evmChain, + onClose, + onSelectChain, +}: Pick< + BridgeWalletSelectProps, + "cosmosChain" | "evmChain" | "direction" | "onSelectChain" +> & { + onClose: () => void; +}) => { + const { accountStore } = useStore(); + const cosmosAccount = cosmosChain + ? accountStore.getWallet(cosmosChain.chainId) + : undefined; + + const [search, setSearch] = useState(""); + const [isManaging, setIsManaging] = useState(false); + + const { connector: evmConnector, isConnected: isEvmWalletConnected } = + useEvmWalletAccount(); + + const { disconnect: disconnectEvmWallet } = useDisconnectEvmWallet(); + + const { + onConnect: onConnectWallet, + wagmi: { + variables: connectingWagmiVariables, + status: connectingWagmiStatus, + error: connectingWagmiError, + }, + } = useConnectWallet({ + walletOptions: [ + ...(cosmosChain?.chainId + ? [{ walletType: "cosmos" as const, chainId: cosmosChain.chainId }] + : []), + ...(evmChain?.chainId + ? [ + { + walletType: "evm" as const, + chainId: evmChain.chainId as EthereumChainIds, + }, + ] + : []), + ], + onConnect: () => { + onSelectChain(evmChain!); + onClose(); + }, + }); + const { evmWallets } = useSelectableWallets({ + includedWallets: ["evm"], + isMobile: false, + }); + + if (!isNil(connectingWagmiVariables?.connector)) { + return ( +
+ +
+ ); + } + + const showEvmWallets = !isNil(evmChain) && !isNil(evmWallets); + + return ( +
+
+ {showEvmWallets && ( + { + setSearch(nextValue); + }} + currentValue={search} + /> + )} + + {(isEvmWalletConnected || !isNil(cosmosAccount)) && ( +
+
+

Your connected wallets

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

Primary wallet

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

Other wallets

+
+ {evmWallets + .filter((wallet) => { + if (wallet.id === evmConnector?.id) return false; // Don't show connected wallet + if (!search) return true; + return wallet.name + .toLowerCase() + .includes(search.toLowerCase()); + }) + .map((wallet) => { + return ( + + onConnectWallet({ + walletType: "evm", + wallet, + chainId: evmChain.chainId as EthereumChainIds, + }) + } + name={wallet.name} + icon={wallet.icon} + /> + ); + })} +
+
+ )} +
+
+ ); +}; + +const WalletButton: React.FC<{ + onClick: () => void; + icon: string | undefined; + name: ReactNode; + suffix?: ReactNode; +}> = ({ onClick, icon, name, suffix }) => { + return ( +
+ + + {suffix} +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/immersive-bridge.tsx b/packages/web/components/bridge/immersive/immersive-bridge.tsx index a7f0df2aac..4b352da241 100644 --- a/packages/web/components/bridge/immersive/immersive-bridge.tsx +++ b/packages/web/components/bridge/immersive/immersive-bridge.tsx @@ -5,11 +5,11 @@ import { memo, PropsWithChildren, useState } from "react"; import { useLockBodyScroll } from "react-use"; import { Icon } from "~/components/assets"; -import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; +import { AmountAndConfirmationScreen } from "~/components/bridge/immersive/amount-and-confirmation-screen"; import { AssetSelectScreen } from "~/components/bridge/immersive/asset-select-screen"; import { Screen, ScreenManager } from "~/components/screen-manager"; import { StepProgress } from "~/components/stepper/progress-bar"; -import { Button, IconButton } from "~/components/ui/button"; +import { IconButton } from "~/components/ui/button"; import { EventName } from "~/config"; import { useTranslation } from "~/hooks"; import { BridgeFlowProvider } from "~/hooks/bridge"; @@ -19,9 +19,8 @@ import { FiatRampKey } from "~/integrations"; import { ModalCloseButton } from "~/modals"; import { FiatOnrampSelectionModal } from "~/modals/fiat-on-ramp-selection"; import { FiatRampsModal } from "~/modals/fiat-ramps"; -import { api } from "~/utils/trpc"; -const enum ImmersiveBridgeScreens { +export const enum ImmersiveBridgeScreens { Asset = "0", Amount = "1", Review = "2", @@ -46,18 +45,6 @@ export const ImmersiveBridgeFlow = ({ const [selectedAssetDenom, setSelectedAssetDenom] = useState(); - const { data: canonicalAssetsWithVariants } = - api.edge.assets.getCanonicalAssetWithVariants.useQuery( - { - findMinDenomOrSymbol: selectedAssetDenom!, - }, - { - enabled: !isNil(selectedAssetDenom), - cacheTime: 10 * 60 * 1000, // 10 minutes - staleTime: 10 * 60 * 1000, // 10 minutes - } - ); - const [fiatRampParams, setFiatRampParams] = useState<{ fiatRampKey: FiatRampKey; assetKey: string; @@ -189,21 +176,11 @@ export const ImmersiveBridgeFlow = ({ /> )} - - - - - {({ goBack }) => ( -
-
Step 3: Review
- - -
- )} -
+
diff --git a/packages/web/components/bridge/immersive/more-bridge-options.tsx b/packages/web/components/bridge/immersive/more-bridge-options.tsx index 32c9f3b5e6..36a07e839d 100644 --- a/packages/web/components/bridge/immersive/more-bridge-options.tsx +++ b/packages/web/components/bridge/immersive/more-bridge-options.tsx @@ -1,4 +1,4 @@ -import { MinimalAsset } from "@osmosis-labs/types"; +import { BridgeAsset, BridgeChain } from "@osmosis-labs/bridge"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import React from "react"; @@ -7,55 +7,48 @@ import { Icon } from "~/components/assets"; import { SkeletonLoader } from "~/components/loaders"; import { useTranslation } from "~/hooks"; import { ModalBase, ModalBaseProps } from "~/modals"; -import { useStore } from "~/stores"; import { api } from "~/utils/trpc"; interface MoreBridgeOptionsProps extends ModalBaseProps { - type: "deposit" | "withdraw"; - asset: MinimalAsset; + direction: "deposit" | "withdraw"; + fromAsset: BridgeAsset | undefined; + toAsset: BridgeAsset | undefined; + fromChain: BridgeChain | undefined; + toChain: BridgeChain | undefined; + toAddress: string | undefined; } export const MoreBridgeOptions = observer( - ({ type, asset, ...modalProps }: MoreBridgeOptionsProps) => { - const { - accountStore, - chainStore: { - osmosis: { prettyChainName }, - }, - } = useStore(); - const wallet = accountStore.getWallet(accountStore.osmosisChainId); + ({ + direction, + fromAsset, + toAsset, + fromChain, + toChain, + toAddress, + ...modalProps + }: MoreBridgeOptionsProps) => { const { t } = useTranslation(); - // TODO: Get fromChain, toChain, and address from props after supportTokens. const { data: externalUrlsData, isLoading: isLoadingExternalUrls } = api.bridgeTransfer.getExternalUrls.useQuery( { - fromAsset: { - address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", - decimals: asset!.coinDecimals, - denom: asset!.coinDenom, - }, - toAsset: { - address: asset!.coinMinimalDenom, - decimals: asset!.coinDecimals, - denom: asset!.coinDenom, - }, - fromChain: { chainId: 43114, chainType: "evm" }, - toChain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - toAddress: wallet?.address ?? "", + fromAsset: fromAsset!, + toAsset: toAsset!, + fromChain: fromChain!, + toChain: toChain!, + toAddress: toAddress!, }, { - enabled: !!asset, + enabled: + !!fromAsset && !!toAsset && !!fromChain && !!toChain && !!toAddress, } ); return (

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

@@ -100,7 +93,7 @@ export const MoreBridgeOptions = observer( /> {t( - type === "deposit" + direction === "deposit" ? "transfer.moreBridgeOptions.depositWith" : "transfer.moreBridgeOptions.withdrawWith" )}{" "} diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts new file mode 100644 index 0000000000..362530dcc2 --- /dev/null +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -0,0 +1,674 @@ +import { CoinPretty, Dec, DecUtils, RatePretty } from "@keplr-wallet/unit"; +import { + Bridge, + BridgeAsset, + BridgeChain, + BridgeError, + CosmosBridgeTransactionRequest, + EvmBridgeTransactionRequest, + GetTransferStatusParams, +} from "@osmosis-labs/bridge"; +import { DeliverTxResponse } from "@osmosis-labs/stores"; +import { isNil } from "@osmosis-labs/utils"; +import dayjs from "dayjs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useDebounce, useUnmount } from "react-use"; +import { Address } from "viem"; +import { BaseError } from "wagmi"; + +import { displayToast } from "~/components/alert/toast"; +import { ToastType } from "~/components/alert/types"; +import { useEvmWalletAccount, useSendEvmTransaction } from "~/hooks/evm-wallet"; +import { useTranslation } from "~/hooks/language"; +import { useStore } from "~/stores"; +import { getWagmiToastErrorMessage } from "~/utils/ethereum"; +import { api, RouterInputs } from "~/utils/trpc"; + +const refetchInterval = 30 * 1000; // 30 seconds + +export const useBridgeQuote = ({ + direction, + + inputAmount: inputAmountRaw, + + sourceAddress, + sourceChain, + sourceAsset, + + destinationAddress, + destinationAsset, + destinationChain, + + bridges = ["Axelar", "Skip", "Squid", "IBC"], + + onRequestClose, + onTransfer: onTransferProp, +}: { + direction: "deposit" | "withdraw"; + + inputAmount: string; + + sourceAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; + sourceChain: BridgeChain | undefined; + sourceAddress: string | undefined; + + destinationAsset: BridgeAsset | undefined; + destinationChain: BridgeChain | undefined; + destinationAddress: string | undefined; + + bridges?: Bridge[]; + + onRequestClose: () => void; + onTransfer?: () => void; +}) => { + const { accountStore, transferHistoryStore, queriesStore } = useStore(); + const { + connector: evmConnector, + address: evmAddress, + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + } = useEvmWalletAccount(); + const { sendTransactionAsync, isLoading: isEthTxPending } = + useSendEvmTransaction(); + const { t } = useTranslation(); + + // In the context of Osmosis, this refers to the Osmosis chain. + const destinationPath = { + address: destinationAddress, + asset: destinationAsset, + chain: destinationChain, + }; + + const sourcePath = { + address: sourceAddress, + asset: sourceAsset, + chain: sourceChain, + }; + + const isDeposit = direction === "deposit"; + const isWithdraw = direction === "withdraw"; + + const quoteParams: Partial< + Omit< + RouterInputs["bridgeTransfer"]["getQuoteByBridge"], + "bridge" | "fromAmount" + > + > = { + fromAddress: isDeposit ? sourcePath.address : destinationPath.address, + fromAsset: isDeposit ? sourcePath.asset : destinationPath.asset, + fromChain: isDeposit ? sourcePath.chain : destinationPath.chain, + toAddress: isDeposit ? destinationPath.address : sourcePath.address, + toAsset: isDeposit ? destinationPath.asset : sourcePath.asset, + toChain: isDeposit ? destinationPath.chain : sourcePath.chain, + }; + + const [selectedBridgeProvider, setSelectedBridgeProvider] = + useState(null); + const [isBridgeProviderControlledMode, setBridgeProviderControlledMode] = + useState(false); + + const onChangeBridgeProvider = useCallback((bridge: Bridge) => { + setSelectedBridgeProvider(bridge); + setBridgeProviderControlledMode(true); + }, []); + + // Input + const [debouncedInputValue, setDebouncedInputValue] = + useState(inputAmountRaw); + useDebounce( + () => { + setDebouncedInputValue(inputAmountRaw); + // Every time the input amount changes, deactivate the controlled mode. + // Best quotes will be selected automatically. + setBridgeProviderControlledMode(false); + }, + 300, + [inputAmountRaw] + ); + const inputAmount = new Dec( + debouncedInputValue === "" ? "0" : debouncedInputValue + ).mul( + // CoinPretty only accepts whole amounts + DecUtils.getTenExponentNInPrecisionRange(destinationAsset?.decimals ?? 0) + ); + + const availableBalance = sourceAsset?.amount; + + const isInsufficientBal = + inputAmountRaw !== "" && + availableBalance && + new CoinPretty(availableBalance.currency, inputAmount) + .toDec() + .gt(availableBalance.toDec()); + + const quoteResults = api.useQueries((t) => + bridges.map((bridge) => + t.bridgeTransfer.getQuoteByBridge( + { + ...(quoteParams as Required), + bridge, + fromAmount: inputAmount.truncate().toString(), + }, + { + enabled: + inputAmount.gt(new Dec(0)) && + Object.values(quoteParams).every((param) => !isNil(param)) && + !isInsufficientBal, + staleTime: 5_000, + cacheTime: 5_000, + // Disable retries, as useQueries + // will block successful quotes from being returned + // if failed quotes are being returned + // until retry starts returning false. + // This causes slow UX even though there's a + // quote that the user can use. + retry: false, + + refetchInterval, // 30 seconds + + select: ({ quote }) => { + const { + estimatedGasFee, + transferFee, + estimatedTime, + expectedOutput, + transactionRequest, + provider, + fromChain, + toChain, + input, + } = quote; + + const priceImpact = new RatePretty( + new Dec(expectedOutput.priceImpact) + ); + const expectedOutputFiatDec = expectedOutput.fiatValue.toDec(); + const inputFiatDec = input.fiatValue.toDec(); + + let transferSlippage: Dec; + if (expectedOutputFiatDec.gt(inputFiatDec)) { + // if expected output is greater than input, assume slippage is 0% + transferSlippage = new Dec(0); + } else if (expectedOutputFiatDec.lt(new Dec(0))) { + // if expected output is negative, assume slippage is 100% + transferSlippage = new Dec(1); + } else { + transferSlippage = new Dec(1).sub( + expectedOutputFiatDec.quo(inputFiatDec) + ); + } + + return { + gasCost: estimatedGasFee + ? new CoinPretty( + { + coinDecimals: estimatedGasFee.decimals, + coinDenom: estimatedGasFee.denom, + coinMinimalDenom: estimatedGasFee.address, + }, + new Dec(estimatedGasFee.amount) + ).maxDecimals(8) + : undefined, + + transferFee: new CoinPretty( + { + coinDecimals: transferFee.decimals, + coinDenom: transferFee.denom, + coinMinimalDenom: transferFee.address, + }, + new Dec(transferFee.amount) + ).maxDecimals(8), + + expectedOutput: new CoinPretty( + { + coinDecimals: expectedOutput.decimals, + coinDenom: expectedOutput.denom, + coinMinimalDenom: expectedOutput.address, + }, + new Dec(expectedOutput.amount) + ), + + expectedOutputFiat: expectedOutput.fiatValue, + transferFeeFiat: transferFee.fiatValue, + gasCostFiat: estimatedGasFee?.fiatValue, + estimatedTime: dayjs.duration({ + seconds: estimatedTime, + }), + responseTime: dayjs(), + quote, + transactionRequest, + priceImpact, + provider, + fromChain, + toChain, + isSlippageTooHigh: transferSlippage.gt(new Dec(0.06)), // warn if expected output is less than 6% of input amount + isPriceImpactTooHigh: priceImpact.toDec().gte(new Dec(0.1)), // warn if price impact is greater than 10%. + }; + }, + + // prevent batching so that fast routers can + // return requests faster than the slowest router + trpc: { + context: { + skipBatch: true, + }, + }, + } + ) + ) + ); + + const successfulQuotes = useMemo(() => { + return quoteResults.filter( + ( + quote + ): quote is typeof quote & { data: NonNullable } => + quote.isSuccess && !isNil(quote.data) + ); + }, [quoteResults]); + + const erroredQuotes = useMemo(() => { + return quoteResults.filter(({ isError }) => isError); + }, [quoteResults]); + + const selectedQuoteQuery = useMemo(() => { + return successfulQuotes.find( + ({ data: quote }) => quote?.provider.id === selectedBridgeProvider + ); + }, [selectedBridgeProvider, successfulQuotes]); + + const selectedQuote = useMemo(() => { + return selectedQuoteQuery?.data; + }, [selectedQuoteQuery]); + + const numSucceeded = successfulQuotes.length; + const isOneSuccessful = Boolean(numSucceeded); + const amountOfErrors = erroredQuotes.length; + const isOneErrored = Boolean(amountOfErrors); + + // if none have returned a resulting quote, find some error + const someError = useMemo( + () => + !isOneSuccessful && + isOneErrored && + quoteResults.every(({ isLoading }) => !isLoading) + ? quoteResults.find((quoteResult) => Boolean(quoteResult.error))?.error + : undefined, + [isOneSuccessful, isOneErrored, quoteResults] + ); + + useEffect(() => { + const quoteResults_ = [...quoteResults]; + + const bestQuote = quoteResults_ + // only those that have fetched + .filter((quoteResult) => Boolean(quoteResult.isFetched)) + // Sort by response time. The fastest and highest quality quote will be first. + .sort((a, b) => { + if (a.data?.responseTime.isBefore(b.data?.responseTime)) { + return 1; + } + if (a.data?.responseTime.isAfter(b.data?.responseTime)) { + return -1; + } + return 0; + }) + // only those that have returned a result without error + .map(({ data }) => data) + // only the best quote data + .reduce((bestAcc, cur) => { + if (!bestAcc) return cur; + if ( + !!cur && + bestAcc.expectedOutput.toDec().lt(cur.expectedOutput.toDec()) + ) { + return cur; + } + return bestAcc; + }, undefined); + + // If the selected bridge provider is not found in the results, select the best quote provider + const isBridgeProviderNotFound = !quoteResults_.some( + ({ data }) => data?.provider.id === selectedBridgeProvider + ); + + if ( + !!bestQuote && + ((bestQuote?.provider.id !== selectedBridgeProvider && + !isBridgeProviderControlledMode) || + isBridgeProviderNotFound) + ) { + setSelectedBridgeProvider(bestQuote.provider.id); + } + }, [ + selectedQuote, + quoteResults, + selectedBridgeProvider, + isBridgeProviderControlledMode, + ]); + + const isInsufficientFee = + inputAmountRaw !== "" && + availableBalance && + selectedQuote?.transferFee !== undefined && + selectedQuote?.transferFee.denom === availableBalance.denom && // make sure the fee is in the same denom as the asset + new CoinPretty(availableBalance.currency, inputAmount) + .toDec() + .sub(availableBalance?.toDec() ?? new Dec(0)) // subtract by available balance to get the maximum transfer amount + .abs() + .lt(selectedQuote?.transferFee.toDec()); + + const bridgeTransaction = + api.bridgeTransfer.getTransactionRequestByBridge.useQuery( + { + ...(quoteParams as Required), + fromAmount: inputAmount.truncate().toString(), + bridge: selectedBridgeProvider!, + }, + { + /** + * If there is no transaction request data, fetch it. + */ + enabled: + Boolean(selectedQuote) && + Boolean(selectedBridgeProvider) && + !selectedQuote?.transactionRequest && + inputAmount.gt(new Dec(0)) && + !isInsufficientBal && + !isInsufficientFee && + Object.values(quoteParams).every((param) => !isNil(param)), + refetchInterval: 30 * 1000, // 30 seconds + } + ); + + useUnmount(() => { + setSelectedBridgeProvider(null); + setBridgeProviderControlledMode(false); + }); + + const [transferInitiated, setTransferInitiated] = useState(false); + const trackTransferStatus = useCallback( + (providerId: Bridge, params: GetTransferStatusParams) => { + if (inputAmountRaw !== "" && availableBalance) { + transferHistoryStore.pushTxNow( + `${providerId}${JSON.stringify(params)}`, + new CoinPretty(availableBalance.currency, inputAmount) + .trim(true) + .toString(), + isWithdraw, + destinationAddress ?? "" // use osmosis account (destinationAddress) for account keys (vs any EVM account) + ); + } + }, + [ + availableBalance, + destinationAddress, + inputAmount, + inputAmountRaw, + isWithdraw, + transferHistoryStore, + ] + ); + + const [isApprovingToken, setIsApprovingToken] = useState(false); + + const isSendTxPending = (() => { + if (!destinationChain) return false; + return destinationChain.chainType === "cosmos" + ? accountStore.getWallet(destinationChain.chainId)?.txTypeInProgress !== + "" + : isEthTxPending; + })(); + + // close modal when initial eth transaction is committed + useEffect(() => { + if (transferInitiated && !isSendTxPending) { + onRequestClose(); + } + }, [isSendTxPending, onRequestClose, transferInitiated]); + + const handleEvmTx = async ( + quote: NonNullable["quote"] + ) => { + if (!isEvmWalletConnected || !evmAddress || !evmConnector) + throw new Error("No ETH wallet account is connected"); + + const transactionRequest = + quote.transactionRequest as EvmBridgeTransactionRequest; + try { + /** + * This occurs when users haven't given permission to the bridge smart contract to use their tokens. + */ + if (transactionRequest.approvalTransactionRequest) { + setIsApprovingToken(true); + + await sendTransactionAsync({ + to: transactionRequest.approvalTransactionRequest.to as Address, + account: evmAddress, + data: transactionRequest.approvalTransactionRequest.data as Address, + }); + + setIsApprovingToken(false); + + for (const quoteResult of quoteResults) { + await quoteResult.refetch(); + } + + return; + } + + const txHash = await sendTransactionAsync({ + to: transactionRequest.to, + account: evmAddress, + value: transactionRequest?.value + ? BigInt(transactionRequest.value) + : undefined, + data: transactionRequest.data, + gas: transactionRequest.gas + ? BigInt(transactionRequest.gas) + : undefined, + gasPrice: transactionRequest.gasPrice + ? BigInt(transactionRequest.gasPrice) + : undefined, + maxFeePerGas: transactionRequest.maxFeePerGas + ? BigInt(transactionRequest.maxFeePerGas) + : undefined, + maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas + ? BigInt(transactionRequest.maxPriorityFeePerGas) + : undefined, + }); + + trackTransferStatus(quote.provider.id, { + sendTxHash: txHash as string, + fromChainId: quote.fromChain.chainId, + toChainId: quote.toChain.chainId, + }); + + // TODO: Investigate if this is still needed + // setLastDepositAccountEvmAddress(ethWalletClient.accountAddress!); + + onTransferProp?.(); + setTransferInitiated(true); + } catch (e) { + const error = e as BaseError; + const toastContent = getWagmiToastErrorMessage({ + error, + t, + walletName: evmConnector.name, + }); + displayToast(toastContent, ToastType.ERROR); + } + }; + + const handleCosmosTx = async ( + quote: NonNullable["quote"] + ) => { + if (!destinationChain || destinationChain?.chainType !== "cosmos") { + throw new Error("Destination chain is not cosmos"); + } + const transactionRequest = + quote.transactionRequest as CosmosBridgeTransactionRequest; + return accountStore.signAndBroadcast( + destinationChain.chainId, + transactionRequest.msgTypeUrl, + [ + { + typeUrl: transactionRequest.msgTypeUrl, + value: transactionRequest.msg, + }, + ], + "", + undefined, + undefined, + (tx: DeliverTxResponse) => { + if (tx.code == null || tx.code === 0) { + const queries = queriesStore.get(destinationChain.chainId); + + // After succeeding to send token, refresh the balance. + const queryBalance = queries.queryBalances + // If we get here destination address is defined + .getQueryBech32Address(destinationAddress!) + .balances.find((bal) => { + return ( + bal.currency.coinMinimalDenom === + availableBalance?.currency.coinMinimalDenom + ); + }); + + if (queryBalance) { + queryBalance.fetch(); + } + + trackTransferStatus(quote.provider.id, { + sendTxHash: tx.transactionHash, + fromChainId: quote.fromChain.chainId, + toChainId: quote.toChain.chainId, + }); + + onTransferProp?.(); + setTransferInitiated(true); + } + } + ); + }; + + const onTransfer = async () => { + const transactionRequest = + selectedQuote?.transactionRequest ?? + bridgeTransaction.data?.transactionRequest; + const quote = selectedQuote?.quote; + + if (!transactionRequest || !quote) return; + + if (transactionRequest.type === "evm") { + await handleEvmTx({ ...quote, transactionRequest }); + } else if (transactionRequest.type === "cosmos") { + await handleCosmosTx({ + ...quote, + transactionRequest, + }); + } + }; + + const hasNoQuotes = someError?.message.includes( + "NoQuotesError" as BridgeError + ); + const warnUserOfSlippage = selectedQuote?.isSlippageTooHigh; + const warnUserOfPriceImpact = selectedQuote?.isPriceImpactTooHigh; + const isCorrectEvmChainSelected = + sourceChain?.chainType === "evm" + ? currentEvmChainId === sourceChain?.chainId + : true; + + let buttonErrorMessage: string | undefined; + if (!sourceAddress) { + buttonErrorMessage = t("assets.transfer.errors.missingAddress"); + } else if (hasNoQuotes) { + buttonErrorMessage = t("assets.transfer.errors.noQuotesAvailable"); + } else if (!isEvmWalletConnected) { + buttonErrorMessage = t("assets.transfer.errors.reconnectWallet", { + walletName: evmConnector?.name ?? "Unknown", + }); + } else if (isDeposit && !isCorrectEvmChainSelected) { + buttonErrorMessage = t("assets.transfer.errors.wrongNetworkInWallet", { + walletName: evmConnector?.name ?? "Unknown", + }); + } else if (Boolean(someError)) { + buttonErrorMessage = t("assets.transfer.errors.unexpectedError"); + } else if (bridgeTransaction.error) { + buttonErrorMessage = t("assets.transfer.errors.transactionError"); + } else if (isInsufficientFee) { + buttonErrorMessage = t("assets.transfer.errors.insufficientFee"); + } else if (isInsufficientBal) { + buttonErrorMessage = t("assets.transfer.errors.insufficientBal"); + } + + /** User can interact with any of the controls on the modal. */ + const isLoadingBridgeQuote = + (!isOneSuccessful || + quoteResults.every((quoteResult) => quoteResult.isLoading)) && + quoteResults.some((quoteResult) => quoteResult.fetchStatus !== "idle"); + const isLoadingBridgeTransaction = + bridgeTransaction.isLoading && bridgeTransaction.fetchStatus !== "idle"; + const isWithdrawReady = isWithdraw && !isSendTxPending; + const isDepositReady = + isDeposit && + !isEvmWalletConnected && + isCorrectEvmChainSelected && + !isLoadingBridgeQuote && + !isEthTxPending; + const userCanInteract = isDepositReady || isWithdrawReady; + + let buttonText: string; + if (buttonErrorMessage) { + buttonText = buttonErrorMessage; + } else if (warnUserOfSlippage || warnUserOfPriceImpact) { + buttonText = t("assets.transfer.transferAnyway"); + } else if (isApprovingToken) { + buttonText = t("assets.transfer.approving"); + } else if (isSendTxPending) { + buttonText = t("assets.transfer.sending"); + } else if ( + selectedQuote?.quote?.transactionRequest?.type === "evm" && + selectedQuote?.quote?.transactionRequest.approvalTransactionRequest && + !isEthTxPending + ) { + buttonText = t("assets.transfer.givePermission"); + } else { + buttonText = + direction === "deposit" + ? t("transfer.reviewDeposit") + : t("transfer.reviewWithdraw"); + } + + if (selectedQuote && !selectedQuote.expectedOutput) { + throw new Error("Expected output is not defined."); + } + + return { + buttonText, + buttonErrorMessage, + + selectedQuoteUpdatedAt: selectedQuoteQuery?.dataUpdatedAt, + refetchInterval, + + userCanInteract, + onTransfer, + + isApprovingToken, + + isInsufficientFee, + isInsufficientBal, + warnUserOfSlippage, + warnUserOfPriceImpact, + + successfulQuotes, + selectedBridgeProvider, + setSelectedBridgeProvider: onChangeBridgeProvider, + + selectedQuote, + isLoadingBridgeQuote, + isLoadingBridgeTransaction, + isRefetchingQuote: selectedQuoteQuery?.isRefetching ?? false, + }; +}; diff --git a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts deleted file mode 100644 index 091d92929d..0000000000 --- a/packages/web/components/bridge/immersive/use-bridge-supported-assets.ts +++ /dev/null @@ -1 +0,0 @@ -export const useBridgeSupportedAssets = () => {}; diff --git a/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts new file mode 100644 index 0000000000..1bdbf0ba2f --- /dev/null +++ b/packages/web/components/bridge/immersive/use-bridges-supported-assets.ts @@ -0,0 +1,201 @@ +import type { Bridge, BridgeChain } from "@osmosis-labs/bridge"; +import { MinimalAsset } from "@osmosis-labs/types"; +import { isNil } from "@osmosis-labs/utils"; +import { useMemo } from "react"; + +import { api, RouterOutputs } from "~/utils/trpc"; + +const bridgeKeys: Bridge[] = ["Skip", "Squid", "Axelar", "IBC"]; + +export const useBridgesSupportedAssets = ({ + assets, + chain, +}: { + assets: MinimalAsset[] | undefined; + chain: BridgeChain; +}) => { + const supportedAssetsResults = api.useQueries((t) => + bridgeKeys.flatMap((bridge) => + (assets ?? []).map((asset) => + t.bridgeTransfer.getSupportedAssetsByBridge( + { + bridge, + asset: { + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + denom: asset.coinDenom, + }, + chain, + }, + { + enabled: !isNil(assets), + staleTime: 30_000, + cacheTime: 30_000, + // Disable retries, as useQueries + // will block successful queries from being returned + // if failed queries are being returned + // until retry starts returning false. + // This causes slow UX even though there's a + // query that the user can use. + retry: false, + + // prevent batching so that fast routers can + // return requests faster than the slowest router + trpc: { + context: { + skipBatch: true, + }, + }, + } + ) + ) + ) + ); + + const successfulQueries = useMemo( + () => + supportedAssetsResults.filter( + (data): data is NonNullable> => + !isNil(data) && data?.isSuccess + ), + [supportedAssetsResults] + ); + + /** + * Aggregate supported assets from all successful queries. + * This would be an object with chain id as key and an array of supported assets as value. + * + * Example: + * { + * 1: [ + * { + * "chainId": 1, + * "chainType": "evm", + * "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + * "denom": "USDC", + * "decimals": 6, + * "sourceDenom": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + * "supportedVariants": [ + * "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + * "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", + * "ibc/231FD77ECCB2DB916D314019DA30FE013202833386B1908A191D16989AD80B5A", + * "ibc/F17C9CA112815613C5B6771047A093054F837C3020CBA59DFFD9D780A8B2984C", + * "ibc/9F9B07EF9AD291167CF5700628145DE1DEB777C2CFC7907553B24446515F6D0E", + * "ibc/6B99DB46AA9FF47162148C1726866919E44A6A5E0274B90912FD17E19A337695", + * "ibc/F08DE332018E8070CC4C68FE06E04E254F527556A614F5F8F9A68AF38D367E45" + * ], + * "supportedProviders": [ + * "Skip", + * "Squid", + * "Axelar" + * ] + * } + * ] + * } + */ + const supportedAssetsByChainId = useMemo(() => { + /** + * Map of supported assets by asset address. This is used to + * merge the supported variants for each input asset. + */ + const assetAddress_supportedVariants: Record> = {}; + + /** + * Map of supported assets by asset address. This is used to + * merge the supported providers for each input asset. + */ + const assetAddress_supportedProviders: Record> = {}; + + type AssetsByChainId = + RouterOutputs["bridgeTransfer"]["getSupportedAssetsByBridge"]["supportedAssets"]["assetsByChainId"]; + + const allAssetsByChainId = successfulQueries.reduce((acc, { data }) => { + if (!data) return acc; + + // Merge all assets from providers by chain id + Object.entries(data.supportedAssets.assetsByChainId).forEach( + ([chainId, assets]) => { + assets.forEach((asset) => { + const { address: rawAddress } = asset; + // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid + const address = rawAddress.toLowerCase(); + + if (!assetAddress_supportedVariants[address]) { + assetAddress_supportedVariants[address] = new Set(); + } + if (!assetAddress_supportedProviders[address]) { + assetAddress_supportedProviders[address] = new Set(); + } + assetAddress_supportedVariants[address].add( + data.supportedAssets.inputAssetAddress + ); + assetAddress_supportedProviders[address].add( + data.supportedAssets.providerName + ); + }); + + acc[chainId] = acc[chainId] ? [...acc[chainId], ...assets] : assets; + } + ); + + return acc; + }, {} as AssetsByChainId); + + const assetEntriesByChainId = Object.entries(allAssetsByChainId).map( + ([chainId, assets]) => [ + chainId, + assets + .map(({ providerName, ...asset }) => ({ + ...asset, + supportedVariants: Array.from( + assetAddress_supportedVariants[asset.address.toLowerCase()] + ), + supportedProviders: Array.from( + assetAddress_supportedProviders[asset.address.toLowerCase()] + ), + })) + + .filter( + (asset, index, originalArray) => + // Make sure the asset has at least one supported variant + asset.supportedVariants.length > 0 && + // Make sure the asset has at least one supported provider + asset.supportedProviders.length > 0 && + // Remove Duplicates + index === + // Use toLowerCase since some providers return addresses in different cases. E.g. Skip and Squid + originalArray.findIndex( + (t) => t.address.toLowerCase() === asset.address.toLowerCase() + ) + ), + ] + ); + + return Object.fromEntries(assetEntriesByChainId) as Record< + keyof AssetsByChainId, + Omit< + AssetsByChainId[string][number] & { + supportedVariants: string[]; + supportedProviders: Bridge[]; + }, + "providerName" + >[] + >; + }, [successfulQueries]); + + const supportedChains = useMemo(() => { + return Array.from( + // Remove duplicate chains + new Map( + successfulQueries + .flatMap(({ data }) => data!.supportedAssets.availableChains) + .map(({ chainId, chainType, prettyName }) => [ + chainId, + { chainId, chainType, prettyName }, + ]) + ).values() + ); + }, [successfulQueries]); + + return { supportedAssetsByChainId, supportedChains }; +}; diff --git a/packages/web/components/buttons/icon-button.tsx b/packages/web/components/buttons/icon-button.tsx index 3d2bd68349..086d3a823d 100644 --- a/packages/web/components/buttons/icon-button.tsx +++ b/packages/web/components/buttons/icon-button.tsx @@ -10,6 +10,8 @@ import { Button } from "~/components/buttons/button"; /** * Renders an icon within a button. + * + * @deprecated Use the iconButton within the ui folder */ export const IconButton = forwardRef< HTMLButtonElement, diff --git a/packages/web/components/drawers/drawer.tsx b/packages/web/components/drawers/drawer.tsx index 6bc4b2344f..7fd852c32d 100644 --- a/packages/web/components/drawers/drawer.tsx +++ b/packages/web/components/drawers/drawer.tsx @@ -1,4 +1,4 @@ -import { Disclosure, Transition } from "@headlessui/react"; +import { Transition } from "@headlessui/react"; import { runIfFn } from "@osmosis-labs/utils"; import classNames from "classnames"; import FocusTrap from "focus-trap-react"; @@ -100,9 +100,7 @@ export const DrawerContent = ( ); }; -export const DrawerOverlay: FunctionComponent< - HTMLProps -> = () => { +export const DrawerOverlay: FunctionComponent = () => { const { isOpen, onClose } = useDrawerProps(); return ( [0]> + props: PropsWithChildren> ) => { const { isOpen, setIsAnimationComplete } = useDrawerProps(); return ( diff --git a/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx b/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx index cac3fcbd5e..85e6b877df 100644 --- a/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx +++ b/packages/web/components/swap-tool/__tests__/swap-tool.spec.tsx @@ -21,6 +21,16 @@ import { appRouter } from "~/server/api/root-router"; jest.mock("next/router", () => jest.requireActual("next-router-mock")); +// Mock the ResizeObserver +const ResizeObserverMock = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Stub the global ResizeObserver +global.ResizeObserver = ResizeObserverMock; + const createCaller = createCallerFactory(appRouter); const caller = createCaller({ /** diff --git a/packages/web/components/wallet-states/switching-network-state.tsx b/packages/web/components/wallet-states/switching-network-state.tsx new file mode 100644 index 0000000000..46253a2578 --- /dev/null +++ b/packages/web/components/wallet-states/switching-network-state.tsx @@ -0,0 +1,40 @@ +import { isNil } from "@osmosis-labs/utils"; +import React from "react"; + +interface SwitchingNetworkStateProps { + walletLogo?: string; + title?: string; + desc?: string; + walletName?: string; +} + +export const SwitchingNetworkState = ({ + walletLogo, + title: titleProp, + desc: descProp, + walletName, +}: SwitchingNetworkStateProps) => { + const title = titleProp ?? "Switching network"; + const desc = + descProp ?? + `Open the ${walletName} browser extension to approve the network switch.`; + + return ( +
+
+ {!!walletLogo && typeof walletLogo === "string" && ( + Wallet logo + )} +
+ +
+ {!isNil(title) && ( +

{title}

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

{desc}

+ )} +
+
+ ); +}; diff --git a/packages/web/config/generate-lists.ts b/packages/web/config/generate-lists.ts index 6c7b58d900..f330e42612 100644 --- a/packages/web/config/generate-lists.ts +++ b/packages/web/config/generate-lists.ts @@ -268,7 +268,13 @@ async function generateAssetListFile({ // remove duplicates return self.indexOf(groupKey) === index; }) - .map((groupKey) => `"${groupKey}"`) + .map( + (groupKey) => + `"${groupKey}" /** Symbols: ${assetList.assets + .filter((asset) => asset.variantGroupKey === groupKey)! + .map((asset) => asset.symbol) + .join(",")} */` + ) .join(" | ")}; `; diff --git a/packages/web/hooks/evm-wallet.ts b/packages/web/hooks/evm-wallet.ts index 0f03675202..55625543bf 100644 --- a/packages/web/hooks/evm-wallet.ts +++ b/packages/web/hooks/evm-wallet.ts @@ -1,8 +1,15 @@ -import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi"; +import { + useAccount, + useConnect, + useDisconnect, + useSendTransaction, + useSwitchChain, +} from "wagmi"; export const useConnectEvmWallet = useConnect; export const useEvmWalletAccount = useAccount; export const useSwitchEvmChain = useSwitchChain; export const useDisconnectEvmWallet = useDisconnect; +export const useSendEvmTransaction = useSendTransaction; export type ConnectEvmWalletReturn = ReturnType; diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 5106e68dae..3d1ec9fa91 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -95,7 +95,6 @@ export const useFeatureFlags = () => { !isMobile && launchdarklyFlags.swapToolSimulateFee && // 1-Click trading is dependent on the swap tool simulate fee flag launchdarklyFlags.oneClickTrading, - newDepositWithdrawFlow: false, _isInitialized: isDevModeWithoutClientID ? true : isInitialized, _isClientIDPresent: !!process.env.NEXT_PUBLIC_LAUNCH_DARKLY_CLIENT_SIDE_ID, } as Record; diff --git a/packages/web/integrations/axelar/transfer.tsx b/packages/web/integrations/axelar/transfer.tsx index 41602380dc..a756d08b07 100644 --- a/packages/web/integrations/axelar/transfer.tsx +++ b/packages/web/integrations/axelar/transfer.tsx @@ -1,9 +1,8 @@ import { Environment } from "@axelar-network/axelarjs-sdk"; import { WalletStatus } from "@cosmos-kit/core"; import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; -import type { SourceChain } from "@osmosis-labs/bridge"; import { basicIbcTransfer } from "@osmosis-labs/stores"; -import { getKeyByValue } from "@osmosis-labs/utils"; +import { AxelarSourceChain, getKeyByValue } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { FunctionComponent, @@ -57,7 +56,7 @@ export const AxelarTransfer: FunctionComponent< isWithdraw: boolean; ethWalletClient: EthWallet; balanceOnOsmosis: IBCBalance; - selectedSourceChainKey: SourceChain; + selectedSourceChainKey: AxelarSourceChain; onRequestClose: () => void; onRequestSwitchWallet: () => void; isTestNet?: boolean; diff --git a/packages/web/integrations/axelar/types.ts b/packages/web/integrations/axelar/types.ts index b854088125..883d219758 100644 --- a/packages/web/integrations/axelar/types.ts +++ b/packages/web/integrations/axelar/types.ts @@ -1,4 +1,5 @@ -import type { SourceChain, SourceChainTokenConfig } from "@osmosis-labs/bridge"; +import type { SourceChainTokenConfig } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { IS_TESTNET } from "~/config"; @@ -12,7 +13,7 @@ export interface AxelarBridgeConfig { */ sourceChainTokens: SourceChainTokenConfig[]; /** Default source chain to be selected. Defaults to first in `sourceChains` if left `undefined`. */ - defaultSourceChainId?: SourceChain; + defaultSourceChainId?: AxelarSourceChain; /** Ex: `uusdc`. NOTE: Will get currency info from `originCurrency` on the IBC balance (from registrar). * See: https://docs.axelar.dev/resources/mainnet#assets */ @@ -34,7 +35,7 @@ export interface AxelarBridgeConfig { * Testnet API: https://axelartest-lcd.quickapi.com/axelar/nexus/v1beta1/chains?status=1 */ export const AxelarChainIds_SourceChainMap: { - [axelarChainIds: string]: SourceChain; + [axelarChainIds: string]: AxelarSourceChain; } = IS_TESTNET ? { aurora: "Aurora Testnet", @@ -64,7 +65,7 @@ export const AxelarChainIds_SourceChainMap: { * EVM-compatible wallets, like Metamask. */ export const EthClientChainIds_SourceChainMap: { - [ethClientChainIds: string]: SourceChain; + [ethClientChainIds: string]: AxelarSourceChain; } = { "Aurora Testnet": "Aurora Testnet", "Avalanche Fuji Testnet": "Avalanche Fuji Testnet", diff --git a/packages/web/integrations/axelar/utils.ts b/packages/web/integrations/axelar/utils.ts index 735f0bd1fb..141ddabf2a 100644 --- a/packages/web/integrations/axelar/utils.ts +++ b/packages/web/integrations/axelar/utils.ts @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { t } from "~/hooks/language/context"; @@ -6,7 +6,7 @@ import { t } from "~/hooks/language/context"; * @deprecated */ export function waitByTransferFromSourceChain( - sourceChain: SourceChain | "Osmosis" + sourceChain: AxelarSourceChain | "Osmosis" ) { switch (sourceChain) { case "Ethereum": diff --git a/packages/web/integrations/ethereum/types.ts b/packages/web/integrations/ethereum/types.ts index fd029e90b5..928d1df2f7 100644 --- a/packages/web/integrations/ethereum/types.ts +++ b/packages/web/integrations/ethereum/types.ts @@ -51,11 +51,4 @@ export const ChainNames: { [chainId: string]: string } = { "0x4e454153": "Aurora Testnet", }; -/** - * Placeholder address for the native tokens like ETH, or AVAX. This is used by protocols to refer to the native token, in order, - * to be handled similarly to other ERC20 tokens. - */ -export const NativeEVMTokenConstantAddress = - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; - export const ChainNetworkConfigs: { [chainId: string]: object } = {}; diff --git a/packages/web/integrations/nomic/transfer.tsx b/packages/web/integrations/nomic/transfer.tsx index 8d14c291af..e900d6e9ec 100644 --- a/packages/web/integrations/nomic/transfer.tsx +++ b/packages/web/integrations/nomic/transfer.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { Network, validate } from "bitcoin-address-validation"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; @@ -43,7 +43,7 @@ export const NomicTransfer: FunctionComponent< { isWithdraw: boolean; balanceOnOsmosis: IBCBalance; - selectedSourceChainKey: SourceChain; + selectedSourceChainKey: AxelarSourceChain; onRequestClose: () => void; onRequestSwitchWallet: () => void; isTestNet?: boolean; diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index bcb0e33d76..274e04b7ed 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Mehr Einzahlungsmöglichkeiten", "moreWithdrawOptions": "Weitere Auszahlungsoptionen", "convertTo": "Konvertieren zu", + "depositAs": "Einzahlung als", + "withdrawAs": "Abheben als", "recommended": "Empfohlen", + "fee": "Gebühr", + "fees": "Gebühren", + "unknown": "Unbekannt", + "unknownFeeTooltip": "Derzeit können Netzwerkgebühren für {networkName} nicht berechnet werden. Netzwerkgebühren sollten nach der Transaktionsgenehmigung in Ihrem Wallet angezeigt werden.", + "free": "Frei", + "cheapest": "Am billigsten", + "fastest": "Am schnellsten", "stepLabels": { "asset": "Vermögenswert", "amount": "Menge", @@ -950,7 +959,16 @@ "completedDeposit": "Einzahlung abgeschlossen", "failedWithdraw": "Auszahlung fehlgeschlagen", "failedDeposit": "Einzahlung fehlgeschlagen", - "connectionError": "Verbindungsfehler" + "connectionError": "Verbindungsfehler", + "transferDetails": "Überweisungsdetails", + "provider": "Anbieter", + "estimatedTime": "Geschätzte Zeit", + "providerFees": "Anbietergebühren", + "networkFee": "{networkName} Netzwerkgebühr", + "totalFees": "Gesamtkosten", + "estimatedAmountReceived": "Geschätzter erhaltener Betrag", + "slippageWarning": "Der erwartete Ausgabebetrag für diese Transaktion ist erheblich niedriger als der Eingabebetrag. Daher kann es sein, dass Sie einen anderen Endbetrag als erwartet erhalten.", + "priceImpactWarning": "Die Preiseinflüsse dieser Transaktion betragen {priceImpact} und können sich auf den endgültigen Betrag auswirken." }, "unknownError": "Unbekannter Fehler", "viewExplorer": "Explorer anzeigen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index f500494907..7051643f84 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -921,7 +921,16 @@ "moreDepositOptions": "More deposit options", "moreWithdrawOptions": "More withdraw options", "convertTo": "Convert to", + "depositAs": "Deposit as", + "withdrawAs": "Withdraw as", "recommended": "Recommended", + "fee": "fee", + "fees": "fees", + "unknown": "Unknown", + "unknownFeeTooltip": "Currently network fees on {networkName} can’t be calculated. Network fees should be displayed in your wallet upon transaction approval.", + "free": "Free", + "cheapest": "Cheapest", + "fastest": "Fastest", "stepLabels": { "asset": "Asset", "amount": "Amount", @@ -950,7 +959,16 @@ "completedDeposit": "Deposit completed", "failedWithdraw": "Withdraw failed", "failedDeposit": "Deposit failed", - "connectionError": "Connection error" + "connectionError": "Connection error", + "transferDetails": "Transfer details", + "provider": "Provider", + "estimatedTime": "Estimated time", + "providerFees": "Provider fees", + "networkFee": "{networkName} Network fee", + "totalFees": "Total fees", + "estimatedAmountReceived": "Estimated amount received", + "slippageWarning": "The expected output for this transaction is significantly lower than the input amount, which may result in receiving a different final amount than expected.", + "priceImpactWarning": "The price impact of this transaction is {priceImpact}, which may influence the final amount received." }, "unknownError": "Unknown error", "viewExplorer": "View explorer", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index c0d0136671..65225fea8a 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Más opciones de depósito", "moreWithdrawOptions": "Más opciones de retiro", "convertTo": "Convertir a", + "depositAs": "Depositar como", + "withdrawAs": "Retirarse como", "recommended": "Recomendado", + "fee": "tarifa", + "fees": "honorarios", + "unknown": "Desconocido", + "unknownFeeTooltip": "Actualmente, las tarifas de red de {networkName} no se pueden calcular. Las tarifas de la red deben aparecer en su billetera al momento de la aprobación de la transacción.", + "free": "Gratis", + "cheapest": "Lo mas barato", + "fastest": "Lo más rápido", "stepLabels": { "asset": "Activo", "amount": "Cantidad", @@ -950,7 +959,16 @@ "completedDeposit": "Depósito completado", "failedWithdraw": "Retiro fallido", "failedDeposit": "El depósito falló", - "connectionError": "Error de conexión" + "connectionError": "Error de conexión", + "transferDetails": "Detalles de la transferencia", + "provider": "Proveedor", + "estimatedTime": "Hora prevista", + "providerFees": "Tarifas del proveedor", + "networkFee": "{networkName} Tarifa de red", + "totalFees": "Tarifas totales", + "estimatedAmountReceived": "Cantidad estimada recibida", + "slippageWarning": "El resultado esperado para esta transacción es significativamente menor que el monto de entrada, lo que puede resultar en recibir un monto final diferente al esperado.", + "priceImpactWarning": "El impacto en el precio de esta transacción es {priceImpact} , lo que puede influir en el importe final recibido." }, "unknownError": "Error desconocido", "viewExplorer": "Ver Explorador", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index e2e13bbb6e..b42d816d69 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -921,7 +921,16 @@ "moreDepositOptions": "گزینه های سپرده بیشتر", "moreWithdrawOptions": "گزینه های برداشت بیشتر", "convertTo": "تبدیل به", + "depositAs": "سپرده گذاری به عنوان", + "withdrawAs": "برداشت به عنوان", "recommended": "توصیه شده", + "fee": "هزینه", + "fees": "هزینه ها", + "unknown": "ناشناخته", + "unknownFeeTooltip": "در حال حاضر هزینه‌های شبکه برای {networkName} قابل محاسبه نیست. پس از تایید تراکنش، هزینه های شبکه باید در کیف پول شما نمایش داده شود.", + "free": "رایگان", + "cheapest": "ارزان ترین", + "fastest": "سریع ترین", "stepLabels": { "asset": "دارایی", "amount": "میزان", @@ -950,7 +959,16 @@ "completedDeposit": "واریز تکمیل شد", "failedWithdraw": "برداشت ناموفق بود", "failedDeposit": "سپرده گذاری انجام نشد", - "connectionError": "خطای اتصال" + "connectionError": "خطای اتصال", + "transferDetails": "جزئیات انتقال", + "provider": "ارائه دهنده", + "estimatedTime": "زمان تخمین زده شده", + "providerFees": "هزینه های ارائه دهنده", + "networkFee": "هزینه شبکه {networkName}", + "totalFees": "مجموع هزینه ها", + "estimatedAmountReceived": "مبلغ تخمینی دریافت شده", + "slippageWarning": "خروجی مورد انتظار برای این تراکنش به طور قابل توجهی کمتر از مقدار ورودی است که ممکن است منجر به دریافت مبلغ نهایی متفاوتی نسبت به انتظار شود.", + "priceImpactWarning": "تأثیر قیمت این تراکنش {priceImpact} است که ممکن است بر مبلغ نهایی دریافتی تأثیر بگذارد." }, "unknownError": "خطای نا شناس", "viewExplorer": "مشاهده جزئیات تراکنش", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index c3a1cf11b7..fd1762d19b 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Plus d'options de dépôt", "moreWithdrawOptions": "Plus d'options de retrait", "convertTo": "Convertir en", + "depositAs": "Dépôt comme", + "withdrawAs": "Se retirer comme", "recommended": "Recommandé", + "fee": "frais", + "fees": "frais", + "unknown": "Inconnu", + "unknownFeeTooltip": "Actuellement, les frais de réseau sur {networkName} ne peuvent pas être calculés. Les frais de réseau doivent être affichés dans votre portefeuille lors de l'approbation de la transaction.", + "free": "Gratuit", + "cheapest": "Le moins cher", + "fastest": "Le plus rapide", "stepLabels": { "asset": "Actif", "amount": "Montant", @@ -950,7 +959,16 @@ "completedDeposit": "Dépôt complété", "failedWithdraw": "Échec du retrait", "failedDeposit": "Échec du dépôt", - "connectionError": "Erreur de connexion" + "connectionError": "Erreur de connexion", + "transferDetails": "Détails du transfert", + "provider": "Fournisseur", + "estimatedTime": "Temps estimé", + "providerFees": "Frais du fournisseur", + "networkFee": "{networkName} Frais de réseau", + "totalFees": "Total des frais", + "estimatedAmountReceived": "Montant estimé reçu", + "slippageWarning": "Le résultat attendu pour cette transaction est nettement inférieur au montant d'entrée, ce qui peut entraîner la réception d'un montant final différent de celui attendu.", + "priceImpactWarning": "L'impact sur le prix de cette transaction est {priceImpact} , ce qui peut influencer le montant final reçu." }, "unknownError": "Erreur inconnue", "viewExplorer": "Voir dans l'exploreur", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 310d9cdc09..3fb49f2b09 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -921,7 +921,16 @@ "moreDepositOptions": "વધુ થાપણ વિકલ્પો", "moreWithdrawOptions": "વધુ ઉપાડ વિકલ્પો", "convertTo": "માં કન્વર્ટ કરો", + "depositAs": "તરીકે જમા", + "withdrawAs": "તરીકે પાછી ખેંચો", "recommended": "ભલામણ કરેલ", + "fee": "ફી", + "fees": "ફી", + "unknown": "અજ્ઞાત", + "unknownFeeTooltip": "હાલમાં {networkName} પરની નેટવર્ક ફીની ગણતરી કરી શકાતી નથી. ટ્રાન્ઝેક્શનની મંજૂરી પર નેટવર્ક ફી તમારા વૉલેટમાં પ્રદર્શિત થવી જોઈએ.", + "free": "મફત", + "cheapest": "સૌથી સસ્તું", + "fastest": "સૌથી ઝડપી", "stepLabels": { "asset": "એસેટ", "amount": "રકમ", @@ -950,7 +959,16 @@ "completedDeposit": "ડિપોઝિટ પૂર્ણ", "failedWithdraw": "ઉપાડ નિષ્ફળ", "failedDeposit": "ડિપોઝિટ નિષ્ફળ", - "connectionError": "કનેક્શન ભૂલ" + "connectionError": "કનેક્શન ભૂલ", + "transferDetails": "ટ્રાન્સફર વિગતો", + "provider": "પ્રદાતા", + "estimatedTime": "અંદાજિત સમય", + "providerFees": "પ્રદાતા ફી", + "networkFee": "{networkName} નેટવર્ક ફી", + "totalFees": "કુલ ફી", + "estimatedAmountReceived": "પ્રાપ્ત થયેલ અંદાજિત રકમ", + "slippageWarning": "આ ટ્રાન્ઝેક્શન માટે અપેક્ષિત આઉટપુટ ઇનપુટ રકમ કરતાં નોંધપાત્ર રીતે ઓછું છે, જેના પરિણામે અપેક્ષા કરતાં અલગ અંતિમ રકમ પ્રાપ્ત થઈ શકે છે.", + "priceImpactWarning": "આ વ્યવહારની કિંમતની અસર {priceImpact} છે, જે પ્રાપ્ત થયેલી અંતિમ રકમને પ્રભાવિત કરી શકે છે." }, "unknownError": "અજાણી ભૂલ", "viewExplorer": "સંશોધક જુઓ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 4573aeb1d0..9a50dbead1 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -921,7 +921,16 @@ "moreDepositOptions": "अधिक जमा विकल्प", "moreWithdrawOptions": "अधिक निकासी विकल्प", "convertTo": "में बदलो", + "depositAs": "इस प्रकार जमा करें", + "withdrawAs": "इस प्रकार वापस लें", "recommended": "अनुशंसित", + "fee": "शुल्क", + "fees": "फीस", + "unknown": "अज्ञात", + "unknownFeeTooltip": "वर्तमान में {networkName} पर नेटवर्क शुल्क की गणना नहीं की जा सकती। लेन-देन स्वीकृति पर नेटवर्क शुल्क आपके वॉलेट में प्रदर्शित होना चाहिए।", + "free": "मुक्त", + "cheapest": "सबसे सस्ता", + "fastest": "सबसे तेजी से", "stepLabels": { "asset": "संपत्ति", "amount": "मात्रा", @@ -950,7 +959,16 @@ "completedDeposit": "जमा पूर्ण हुआ", "failedWithdraw": "निकासी विफल", "failedDeposit": "जमा विफल", - "connectionError": "संपर्क त्रुटि" + "connectionError": "संपर्क त्रुटि", + "transferDetails": "स्थानांतरण विवरण", + "provider": "प्रदाता", + "estimatedTime": "अनुमानित समय", + "providerFees": "प्रदाता शुल्क", + "networkFee": "{networkName} नेटवर्क शुल्क", + "totalFees": "कुल शुल्क", + "estimatedAmountReceived": "अनुमानित प्राप्त राशि", + "slippageWarning": "इस लेनदेन के लिए अपेक्षित आउटपुट इनपुट राशि से काफी कम है, जिसके परिणामस्वरूप अपेक्षित राशि से भिन्न अंतिम राशि प्राप्त हो सकती है।", + "priceImpactWarning": "इस लेनदेन का मूल्य प्रभाव {priceImpact} है, जो प्राप्त अंतिम राशि को प्रभावित कर सकता है।" }, "unknownError": "अज्ञात त्रुटि", "viewExplorer": "एक्सप्लोरर देखें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 780bbdd5e1..130c525a7c 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -921,7 +921,16 @@ "moreDepositOptions": "その他の入金オプション", "moreWithdrawOptions": "引き出しオプションの追加", "convertTo": "に変換", + "depositAs": "入金", + "withdrawAs": "撤退する", "recommended": "推奨", + "fee": "手数料", + "fees": "手数料", + "unknown": "未知", + "unknownFeeTooltip": "現在、 {networkName}のネットワーク手数料を計算できません。ネットワーク手数料は、トランザクションが承認されるとウォレットに表示されます。", + "free": "無料", + "cheapest": "最も安い", + "fastest": "最速", "stepLabels": { "asset": "資産", "amount": "額", @@ -950,7 +959,16 @@ "completedDeposit": "入金完了", "failedWithdraw": "引き出しに失敗しました", "failedDeposit": "入金に失敗しました", - "connectionError": "接続エラー" + "connectionError": "接続エラー", + "transferDetails": "振込詳細", + "provider": "プロバイダー", + "estimatedTime": "予定時刻", + "providerFees": "プロバイダー料金", + "networkFee": "{networkName}ネットワーク料金", + "totalFees": "合計料金", + "estimatedAmountReceived": "受け取る推定金額", + "slippageWarning": "このトランザクションの予想出力は入力金額よりも大幅に低いため、予想とは異なる最終金額を受け取る可能性があります。", + "priceImpactWarning": "この取引の価格影響は{priceImpact}であり、最終的に受け取る金額に影響する可能性があります。" }, "unknownError": "不明なエラー", "viewExplorer": "エクスプローラーを表示する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 6145719261..5a7bd3ffae 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -921,7 +921,16 @@ "moreDepositOptions": "더 많은 입금 옵션", "moreWithdrawOptions": "더 많은 인출 옵션", "convertTo": "로 변환하다", + "depositAs": "다음으로 입금", + "withdrawAs": "다음으로 인출", "recommended": "추천", + "fee": "요금", + "fees": "수수료", + "unknown": "알려지지 않은", + "unknownFeeTooltip": "현재 {networkName} 의 네트워크 요금을 계산할 수 없습니다. 네트워크 수수료는 거래 승인 시 지갑에 표시되어야 합니다.", + "free": "무료", + "cheapest": "가장 저렴", + "fastest": "가장 빠른", "stepLabels": { "asset": "유산", "amount": "양", @@ -950,7 +959,16 @@ "completedDeposit": "입금완료", "failedWithdraw": "출금 실패", "failedDeposit": "입금 실패", - "connectionError": "연결 오류" + "connectionError": "연결 오류", + "transferDetails": "송금 세부정보", + "provider": "공급자", + "estimatedTime": "예상 시간", + "providerFees": "공급자 수수료", + "networkFee": "{networkName} 네트워크 요금", + "totalFees": "총 수수료", + "estimatedAmountReceived": "예상 수령 금액", + "slippageWarning": "이 거래에 대한 예상 출력은 입력 금액보다 상당히 낮으므로 예상과 다른 최종 금액을 받게 될 수 있습니다.", + "priceImpactWarning": "이 거래의 가격 영향은 {priceImpact} 이며, 최종 수령 금액에 영향을 미칠 수 있습니다." }, "unknownError": "알 수 없는 에러", "viewExplorer": "블록 익스플로러 보기", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index f3f170fa77..79d29e2c07 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Więcej opcji depozytu", "moreWithdrawOptions": "Więcej opcji wypłat", "convertTo": "Konwertuj na", + "depositAs": "Wpłać jako", + "withdrawAs": "Wycofaj jako", "recommended": "Zalecana", + "fee": "opłata", + "fees": "opłaty", + "unknown": "Nieznany", + "unknownFeeTooltip": "Obecnie nie można obliczyć opłat sieciowych dla {networkName} . Opłaty sieciowe powinny zostać wyświetlone w Twoim portfelu po zatwierdzeniu transakcji.", + "free": "Bezpłatny", + "cheapest": "Najtańszy", + "fastest": "Najszybszy", "stepLabels": { "asset": "Zaleta", "amount": "Kwota", @@ -950,7 +959,16 @@ "completedDeposit": "Depozyt zrealizowany", "failedWithdraw": "Wypłata nie powiodła się", "failedDeposit": "Wpłata nie powiodła się", - "connectionError": "Błąd połączenia" + "connectionError": "Błąd połączenia", + "transferDetails": "Szczegóły przelewu", + "provider": "Dostawca", + "estimatedTime": "Szacowany czas", + "providerFees": "Opłaty dostawcy", + "networkFee": "{networkName} Opłata sieciowa", + "totalFees": "Wszystkie koszty", + "estimatedAmountReceived": "Szacunkowa otrzymana kwota", + "slippageWarning": "Oczekiwany wynik dla tej transakcji jest znacznie niższy niż kwota wejściowa, co może skutkować otrzymaniem kwoty końcowej innej niż oczekiwano.", + "priceImpactWarning": "Wpływ cenowy tej transakcji to {priceImpact} , co może mieć wpływ na ostateczną otrzymaną kwotę." }, "unknownError": "Nieznany błąd", "viewExplorer": "zobacz eksplorer", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 2672139ca9..d4a0f78309 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Mais opções de depósito", "moreWithdrawOptions": "Mais opções de retirada", "convertTo": "Converter para", + "depositAs": "Depositar como", + "withdrawAs": "Retirar como", "recommended": "Recomendado", + "fee": "taxa", + "fees": "tarifas", + "unknown": "Desconhecido", + "unknownFeeTooltip": "Atualmente, as taxas de rede em {networkName} não podem ser calculadas. As taxas de rede devem ser exibidas em sua carteira após a aprovação da transação.", + "free": "Livre", + "cheapest": "Mais barato", + "fastest": "O mais rápido", "stepLabels": { "asset": "Ativo", "amount": "Quantia", @@ -950,7 +959,16 @@ "completedDeposit": "Depósito concluído", "failedWithdraw": "Falha na retirada", "failedDeposit": "Falha no depósito", - "connectionError": "Erro de conexão" + "connectionError": "Erro de conexão", + "transferDetails": "Detalhes da transferência", + "provider": "Fornecedor", + "estimatedTime": "Tempo estimado", + "providerFees": "Taxas do provedor", + "networkFee": "{networkName} Taxa de rede", + "totalFees": "Taxas totais", + "estimatedAmountReceived": "Valor estimado recebido", + "slippageWarning": "O resultado esperado para esta transação é significativamente inferior ao valor do insumo, o que pode resultar no recebimento de um valor final diferente do esperado.", + "priceImpactWarning": "O impacto no preço desta transação é {priceImpact} , o que pode influenciar o valor final recebido." }, "unknownError": "Erro desconhecido", "viewExplorer": "Visualizar explorer", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index f0fbfbdd0c..0628a369b7 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Mai multe opțiuni de depunere", "moreWithdrawOptions": "Mai multe opțiuni de retragere", "convertTo": "Schimba in", + "depositAs": "Depozit ca", + "withdrawAs": "Retrage ca", "recommended": "Recomandat", + "fee": "taxa", + "fees": "taxe", + "unknown": "Necunoscut", + "unknownFeeTooltip": "În prezent, taxele de rețea pentru {networkName} nu pot fi calculate. Taxele de rețea ar trebui să fie afișate în portofel după aprobarea tranzacției.", + "free": "Gratuit", + "cheapest": "Cele mai ieftine", + "fastest": "Cel mai rapid", "stepLabels": { "asset": "Atu", "amount": "Cantitate", @@ -950,7 +959,16 @@ "completedDeposit": "Depozit finalizat", "failedWithdraw": "Retragerea a eșuat", "failedDeposit": "Depunerea nu a reușit", - "connectionError": "Eroare de conexiune" + "connectionError": "Eroare de conexiune", + "transferDetails": "Detalii transfer", + "provider": "Furnizor", + "estimatedTime": "Timpul estimat", + "providerFees": "Taxe de furnizor", + "networkFee": "{networkName} Taxă de rețea", + "totalFees": "Taxele totale", + "estimatedAmountReceived": "Suma estimată primită", + "slippageWarning": "Ieșirea așteptată pentru această tranzacție este semnificativ mai mică decât suma de intrare, ceea ce poate duce la primirea unei alte sume finale decât cele așteptate.", + "priceImpactWarning": "Impactul asupra prețului acestei tranzacții este {priceImpact} , care poate influența suma finală primită." }, "unknownError": "Eroare necunoscuta", "viewExplorer": "vezi explorer", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 711873b12b..76a2729f38 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Больше вариантов депозита", "moreWithdrawOptions": "Больше вариантов вывода", "convertTo": "Перевести в", + "depositAs": "Депозит как", + "withdrawAs": "Вывести как", "recommended": "рекомендуемые", + "fee": "платеж", + "fees": "сборы", + "unknown": "Неизвестный", + "unknownFeeTooltip": "В настоящее время сетевые сборы для {networkName} не могут быть рассчитаны. Сетевые комиссии должны отображаться в вашем кошельке после одобрения транзакции.", + "free": "Бесплатно", + "cheapest": "Самый дешевый", + "fastest": "Самый быстрый", "stepLabels": { "asset": "Объект", "amount": "Количество", @@ -950,7 +959,16 @@ "completedDeposit": "Депозит завершен", "failedWithdraw": "Вывести не удалось", "failedDeposit": "Депозит не выполнен", - "connectionError": "Ошибка подключения" + "connectionError": "Ошибка подключения", + "transferDetails": "Детали трансфера", + "provider": "Поставщик", + "estimatedTime": "Расчетное время", + "providerFees": "Комиссия поставщика", + "networkFee": "{networkName} Сетевая плата", + "totalFees": "Общая сумма сборов", + "estimatedAmountReceived": "Ориентировочная полученная сумма", + "slippageWarning": "Ожидаемый результат этой транзакции значительно ниже входной суммы, что может привести к получению другой конечной суммы, чем ожидалось.", + "priceImpactWarning": "Влияние этой транзакции на цену составляет {priceImpact} , что может повлиять на конечную полученную сумму." }, "unknownError": "Неизвестная ошибка", "viewExplorer": "Посмотреть проводник", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index ea0bcb1a63..36d18c4961 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -921,7 +921,16 @@ "moreDepositOptions": "Daha fazla para yatırma seçeneği", "moreWithdrawOptions": "Daha fazla para çekme seçeneği", "convertTo": "E dönüşmek", + "depositAs": "Şu şekilde yatırın:", + "withdrawAs": "Olarak geri çekil", "recommended": "Tavsiye edilen", + "fee": "ücret", + "fees": "ücretler", + "unknown": "Bilinmeyen", + "unknownFeeTooltip": "Şu anda {networkName} ağ ücretleri hesaplanamıyor. İşlem onaylandıktan sonra ağ ücretleri cüzdanınızda görüntülenmelidir.", + "free": "Özgür", + "cheapest": "En ucuz", + "fastest": "En hızlı", "stepLabels": { "asset": "Varlık", "amount": "Miktar", @@ -950,7 +959,16 @@ "completedDeposit": "Para yatırma tamamlandı", "failedWithdraw": "Para çekme işlemi başarısız oldu", "failedDeposit": "Para yatırma işlemi başarısız oldu", - "connectionError": "Bağlantı hatası" + "connectionError": "Bağlantı hatası", + "transferDetails": "Aktarım ayrıntıları", + "provider": "Sağlayıcı", + "estimatedTime": "Tahmini süresi", + "providerFees": "Sağlayıcı ücretleri", + "networkFee": "{networkName} Ağ ücreti", + "totalFees": "Toplam ücretler", + "estimatedAmountReceived": "Alınan tahmini miktar", + "slippageWarning": "Bu işlem için beklenen çıktı, girdi tutarından önemli ölçüde düşüktür ve bu, beklenenden farklı bir nihai tutarın alınmasıyla sonuçlanabilir.", + "priceImpactWarning": "Bu işlemin fiyat etkisi {priceImpact} olup, alınan son tutarı etkileyebilir." }, "unknownError": "Bilinmeyen hata", "viewExplorer": "gezginde görüntüle", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 0f7a530c65..dd366acfb6 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -921,7 +921,16 @@ "moreDepositOptions": "更多存款选择", "moreWithdrawOptions": "更多提款选项", "convertTo": "转换成", + "depositAs": "存款为", + "withdrawAs": "提款方式", "recommended": "受到推崇的", + "fee": "费用", + "fees": "费用", + "unknown": "未知", + "unknownFeeTooltip": "目前无法计算{networkName}上的网络费用。交易批准后,网络费用应显示在您的钱包中。", + "free": "自由的", + "cheapest": "最便宜的", + "fastest": "最快的", "stepLabels": { "asset": "资产", "amount": "数量", @@ -950,7 +959,16 @@ "completedDeposit": "存款已完成", "failedWithdraw": "提现失败", "failedDeposit": "存款失败", - "connectionError": "连接错误" + "connectionError": "连接错误", + "transferDetails": "转账详情", + "provider": "提供者", + "estimatedTime": "预计时间", + "providerFees": "供应商费用", + "networkFee": "{networkName}网络费", + "totalFees": "总费用", + "estimatedAmountReceived": "预计收到金额", + "slippageWarning": "该交易的预期输出明显低于输入金额,这可能导致收到的最终金额与预期不同。", + "priceImpactWarning": "此交易的价格影响为{priceImpact} ,可能会影响最终收到的金额。" }, "unknownError": "未知错误", "viewExplorer": "浏览器查看", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index f6482124e1..9f3181cd71 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -921,7 +921,16 @@ "moreDepositOptions": "更多存款選擇", "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", + "depositAs": "存款為", + "withdrawAs": "提款為", "recommended": "受到推崇的", + "fee": "費用", + "fees": "費用", + "unknown": "未知", + "unknownFeeTooltip": "目前無法計算{networkName}上的網路費用。交易批准後,網路費用應顯示在您的錢包中。", + "free": "自由的", + "cheapest": "最便宜", + "fastest": "最快的", "stepLabels": { "asset": "資產", "amount": "數量", @@ -950,7 +959,16 @@ "completedDeposit": "存款完成", "failedWithdraw": "提現失敗", "failedDeposit": "存款失敗", - "connectionError": "連線錯誤" + "connectionError": "連線錯誤", + "transferDetails": "轉帳詳情", + "provider": "提供者", + "estimatedTime": "預計時間", + "providerFees": "提供者費用", + "networkFee": "{networkName}網路費用", + "totalFees": "總費用", + "estimatedAmountReceived": "預計收到金額", + "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 7d53cf46f8..c9b03c3b24 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -921,7 +921,16 @@ "moreDepositOptions": "更多存款選擇", "moreWithdrawOptions": "更多提款選項", "convertTo": "轉換成", + "depositAs": "存款為", + "withdrawAs": "提款為", "recommended": "受到推崇的", + "fee": "費用", + "fees": "費用", + "unknown": "未知", + "unknownFeeTooltip": "目前無法計算{networkName}上的網路費用。交易批准後,網路費用應顯示在您的錢包中。", + "free": "自由的", + "cheapest": "最便宜", + "fastest": "最快的", "stepLabels": { "asset": "資產", "amount": "數量", @@ -950,7 +959,16 @@ "completedDeposit": "存款完成", "failedWithdraw": "提現失敗", "failedDeposit": "存款失敗", - "connectionError": "連線錯誤" + "connectionError": "連線錯誤", + "transferDetails": "轉帳詳情", + "provider": "提供者", + "estimatedTime": "預計時間", + "providerFees": "提供者費用", + "networkFee": "{networkName}網路費用", + "totalFees": "總費用", + "estimatedAmountReceived": "預計收到金額", + "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/modals/bridge-transfer-v1.tsx b/packages/web/modals/bridge-transfer-v1.tsx index 4c116feadb..77d7eb2821 100644 --- a/packages/web/modals/bridge-transfer-v1.tsx +++ b/packages/web/modals/bridge-transfer-v1.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import { FunctionComponent } from "react"; @@ -39,7 +39,7 @@ export const BridgeTransferV1Modal: FunctionComponent< isWithdraw: boolean; balance: IBCBalance; /** Selected network key. */ - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; walletClient: ObservableWallet | undefined; onRequestSwitchWallet: () => void; } diff --git a/packages/web/modals/bridge-transfer-v2.tsx b/packages/web/modals/bridge-transfer-v2.tsx index cf0757bed9..8dfe0b3944 100644 --- a/packages/web/modals/bridge-transfer-v2.tsx +++ b/packages/web/modals/bridge-transfer-v2.tsx @@ -6,12 +6,15 @@ import type { CosmosBridgeTransactionRequest, EvmBridgeTransactionRequest, GetTransferStatusParams, - SourceChain, SourceChainTokenConfig, } from "@osmosis-labs/bridge"; import { DeliverTxResponse } from "@osmosis-labs/stores"; import { Currency } from "@osmosis-labs/types"; -import { getKeyByValue } from "@osmosis-labs/utils"; +import { + AxelarSourceChain, + getKeyByValue, + NativeEVMTokenConstantAddress, +} from "@osmosis-labs/utils"; import { noop } from "@osmosis-labs/utils"; import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; @@ -45,7 +48,6 @@ import { import { ChainNames, EthWallet, - NativeEVMTokenConstantAddress, useErc20Balance, useNativeBalance, useTxReceiptState, @@ -63,7 +65,7 @@ interface BridgeTransferContext { useNativeToken: boolean; setUseWrappedToken: (nextValue: boolean) => void; sourceChainConfig?: SourceChainTokenConfig; - sourceChainKeyMapped: SourceChain; + sourceChainKeyMapped: AxelarSourceChain; originCurrency: Currency; } @@ -76,7 +78,7 @@ const [BridgeTransferModalProvider, useBridgeTransfer] = interface BridgeTransferModalProps extends ModalBaseProps { isWithdraw: boolean; balance: IBCBalance; - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; walletClient?: ObservableWallet; onRequestSwitchWallet: () => void; } @@ -321,7 +323,7 @@ export const TransferContent: FunctionComponent< isWithdraw: boolean; balance: IBCBalance; /** Selected network key. */ - sourceChainKey: SourceChain; + sourceChainKey: AxelarSourceChain; onRequestSwitchWallet: () => void; counterpartyAddress: string; isCounterpartyAddressValid?: boolean; diff --git a/packages/web/modals/select-asset-source.tsx b/packages/web/modals/select-asset-source.tsx index 2f4b1e2762..60552f1aad 100644 --- a/packages/web/modals/select-asset-source.tsx +++ b/packages/web/modals/select-asset-source.tsx @@ -1,4 +1,4 @@ -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { FunctionComponent, useState } from "react"; @@ -16,7 +16,7 @@ import { ModalBase, ModalBaseProps } from "~/modals/base"; export const SelectAssetSourceModal: FunctionComponent< ModalBaseProps & { initiallySelectedWalletId?: string; - desiredSourceKey?: SourceChain; + desiredSourceKey?: AxelarSourceChain; isWithdraw: boolean; wallets: ObservableWallet[]; fiatRamps?: FiatRampKey[]; diff --git a/packages/web/modals/transfer-asset-select.tsx b/packages/web/modals/transfer-asset-select.tsx index e808894320..e82eea8d7b 100644 --- a/packages/web/modals/transfer-asset-select.tsx +++ b/packages/web/modals/transfer-asset-select.tsx @@ -1,5 +1,5 @@ import { CoinPretty } from "@keplr-wallet/unit"; -import type { SourceChain } from "@osmosis-labs/bridge"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import Image from "next/image"; @@ -26,7 +26,7 @@ export const TransferAssetSelectModal: FunctionComponent< onSelectAsset: ( denom: string, /** `undefined` if IBC asset. */ - sourceChainKey?: SourceChain + sourceChainKey?: AxelarSourceChain ) => void; } > = observer((props) => { @@ -52,7 +52,7 @@ export const TransferAssetSelectModal: FunctionComponent< )?.token.denom) || tokens[0].token.denom ); const [selectedSourceChainKey, setSelectedSourceChainKey] = - useState(null); + useState(null); // set network-select to selected token's default useEffect(() => { diff --git a/packages/web/modals/wallet-select/cosmos-wallet-state.tsx b/packages/web/modals/wallet-select/cosmos-wallet-state.tsx index 0831cf3cdc..0b8c1996c3 100644 --- a/packages/web/modals/wallet-select/cosmos-wallet-state.tsx +++ b/packages/web/modals/wallet-select/cosmos-wallet-state.tsx @@ -23,7 +23,8 @@ import { CosmosWalletRegistry } from "~/config"; import { useFeatureFlags, useTranslation, WalletSelectOption } from "~/hooks"; import { useHasInstalledCosmosWallets } from "~/hooks/use-has-installed-wallets"; import { WalletSelectModalProps } from "~/modals/wallet-select"; -import { ModalView, OnConnectWallet } from "~/modals/wallet-select/utils"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { ModalView } from "~/modals/wallet-select/utils"; import { useStore } from "~/stores"; import { QRCodeView } from "./qr-code-view"; diff --git a/packages/web/modals/wallet-select/evm-wallet-state.tsx b/packages/web/modals/wallet-select/evm-wallet-state.tsx index 5fcbe81691..233162afb8 100644 --- a/packages/web/modals/wallet-select/evm-wallet-state.tsx +++ b/packages/web/modals/wallet-select/evm-wallet-state.tsx @@ -7,7 +7,7 @@ import { ErrorWalletState } from "~/components/wallet-states"; import { useTranslation } from "~/hooks"; import { ConnectEvmWalletReturn } from "~/hooks/evm-wallet"; import { WalletSelectModalProps } from "~/modals/wallet-select"; -import { OnConnectWallet } from "~/modals/wallet-select/utils"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; export const EvmWalletState: FunctionComponent< Pick & { diff --git a/packages/web/modals/wallet-select/full-wallet-list.tsx b/packages/web/modals/wallet-select/full-wallet-list.tsx index 070611c85f..693764ec38 100644 --- a/packages/web/modals/wallet-select/full-wallet-list.tsx +++ b/packages/web/modals/wallet-select/full-wallet-list.tsx @@ -4,8 +4,9 @@ import { observer } from "mobx-react-lite"; import React, { FunctionComponent, useMemo } from "react"; import { useTranslation, WalletSelectOption } from "~/hooks"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; -import { ModalView, OnConnectWallet } from "~/modals/wallet-select/utils"; +import { ModalView } from "~/modals/wallet-select/utils"; export const FullWalletList: FunctionComponent<{ walletRepo: WalletRepo | undefined; diff --git a/packages/web/modals/wallet-select/index.tsx b/packages/web/modals/wallet-select/index.tsx index 9e765c7fb7..d58f314dac 100644 --- a/packages/web/modals/wallet-select/index.tsx +++ b/packages/web/modals/wallet-select/index.tsx @@ -1,16 +1,6 @@ -import { - ChainWalletBase, - State, - WalletRepo, - WalletStatus, -} from "@cosmos-kit/core"; -import { - CosmosKitAccountsLocalStorageKey, - CosmosKitWalletLocalStorageKey, - CosmosRegistryWallet, -} from "@osmosis-labs/stores"; +import { State, WalletRepo, WalletStatus } from "@cosmos-kit/core"; import { OneClickTradingTransactionParams } from "@osmosis-labs/types"; -import { isNil, noop } from "@osmosis-labs/utils"; +import { isNil } from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import React, { FunctionComponent, useEffect, useState } from "react"; @@ -20,19 +10,9 @@ import { Connector } from "wagmi"; import { Icon } from "~/components/assets"; import { ClientOnly } from "~/components/client-only"; import { Button } from "~/components/ui/button"; -import { CosmosWalletRegistry } from "~/config"; -import { EthereumChainIds } from "~/config/wagmi"; -import { - useFeatureFlags, - WalletSelectOption, - WalletSelectParams, -} from "~/hooks"; +import { useFeatureFlags, WalletSelectParams } from "~/hooks"; import { useWindowSize } from "~/hooks"; -import { useConnectEvmWallet } from "~/hooks/evm-wallet"; -import { - CreateOneClickSessionError, - useCreateOneClickTradingSession, -} from "~/hooks/mutations/one-click-trading"; +import { useCreateOneClickTradingSession } from "~/hooks/mutations/one-click-trading"; import { useOneClickTradingParams } from "~/hooks/one-click-trading/use-one-click-trading-params"; import { useHasInstalledCosmosWallets } from "~/hooks/use-has-installed-wallets"; import { ModalBase, ModalBaseProps, ModalCloseButton } from "~/modals/base"; @@ -40,13 +20,8 @@ import { CosmosWalletState } from "~/modals/wallet-select/cosmos-wallet-state"; import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; import { FullWalletList } from "~/modals/wallet-select/full-wallet-list"; import { SimpleWalletList } from "~/modals/wallet-select/simple-wallet-list"; -import { WagmiWalletConnectType } from "~/modals/wallet-select/use-selectable-wallets"; -import { - getModalView, - ModalView, - OnConnectWallet, -} from "~/modals/wallet-select/utils"; -import { useStore } from "~/stores"; +import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; +import { getModalView, ModalView } from "~/modals/wallet-select/utils"; export interface WalletSelectModalProps extends ModalBaseProps { /** @@ -67,18 +42,10 @@ export const WalletSelectModal: FunctionComponent = layout = "full", } = props; const { isMobile } = useWindowSize(); - const { accountStore, chainStore } = useStore(); const featureFlags = useFeatureFlags(); const hasInstalledWallets = useHasInstalledCosmosWallets(); const [show1CTEditParams, setShow1CTEditParams] = useState(false); const [hasBroadcastedTx, setHasBroadcastedTx] = useState(false); - const { - connectAsync: connectEvmWallet, - variables, - status, - error, - reset, - } = useConnectEvmWallet(); const create1CTSession = useCreateOneClickTradingSession({ onBroadcasted: () => { @@ -100,8 +67,7 @@ export const WalletSelectModal: FunctionComponent = const [modalView, setModalView] = useState("list"); const [isInitializingOneClickTrading, setIsInitializingOneClickTrading] = useState(false); - const [lazyWalletInfo, setLazyWalletInfo] = - useState<(typeof CosmosWalletRegistry)[number]>(); + const [show1CTConnectAWallet, setShow1CTConnectAWallet] = useState(false); const hasOneClickTradingError = !!create1CTSession.error; @@ -114,20 +80,23 @@ export const WalletSelectModal: FunctionComponent = reset: reset1CTParams, } = useOneClickTradingParams(); - const cosmosOption = walletOptions.find( - ( - option - ): option is Extract => - option.walletType === "cosmos" - ); + const { + onConnect: onConnectWallet, + wagmi: { variables, status, error, reset }, + cosmos: { walletRepo: rootWalletRepo, lazyWalletInfo }, + } = useConnectWallet({ + onConnecting: () => setModalView("connecting"), + onRequestClose: onRequestClose, + walletOptions, + onConnect: onConnectProp, - const cosmosChainId = cosmosOption?.chainId; - const rootWalletRepo = cosmosChainId - ? accountStore.getWalletRepo(cosmosChainId) - : undefined; - const current = rootWalletRepo?.current; - const cosmosWalletStatus = current?.walletStatus; - const cosmosChainName = rootWalletRepo?.chainRecord.chain?.chain_name!; + isOneClickEnabled: transaction1CTParams?.isOneClickEnabled, + onCreate1CTSession: ({ walletRepo }) => + onCreate1CTSession({ walletRepo, transaction1CTParams }), + }); + + const currentCosmosWallet = rootWalletRepo?.current; + const cosmosWalletStatus = currentCosmosWallet?.walletStatus; useEffect(() => { if (isOpen) { @@ -157,7 +126,7 @@ export const WalletSelectModal: FunctionComponent = } }, [isOpen]); - (current?.client as any)?.setActions?.({ + (currentCosmosWallet?.client as any)?.setActions?.({ qrUrl: { state: setQRState, /** @@ -196,144 +165,6 @@ export const WalletSelectModal: FunctionComponent = }); }; - const onConnectCosmosWallet = async ({ - wallet, - walletRepo: walletRepoParam, - }: { - wallet: CosmosRegistryWallet | ChainWalletBase; - walletRepo: WalletRepo; - }) => { - if (current) { - await current?.disconnect(true); - } - - const handleConnectError = (e: Error) => { - console.error("Error while connecting to wallet. Details: ", e); - localStorage.removeItem(CosmosKitWalletLocalStorageKey); - localStorage.removeItem(CosmosKitAccountsLocalStorageKey); - }; - - if (!("lazyInstall" in wallet)) { - wallet - .connect(false) - .then(() => { - onConnectProp?.({ walletType: "cosmos" }); - }) - .catch(handleConnectError); - return; - } - - const isWalletInstalled = rootWalletRepo?.wallets.some( - ({ walletName }) => walletName === wallet.name - ); - - let walletRepo: WalletRepo; - - // if wallet is not installed, install it - if (!isWalletInstalled && "lazyInstall" in wallet) { - setLazyWalletInfo(wallet); - setModalView("connecting"); - - // wallet is now walletInfo - const walletInfo = wallet; - const WalletClass = await wallet.lazyInstall(); - - const walletManager = await accountStore.addWallet( - new WalletClass(walletInfo) - ); - await walletManager.onMounted().catch(handleConnectError); - setLazyWalletInfo(undefined); - - walletRepo = walletManager.getWalletRepo(cosmosChainName!); - } else { - walletRepo = walletRepoParam; - } - - const isOsmosisConnection = - chainStore.osmosis.chainName === cosmosChainName!; - const osmosisWalletRepo = accountStore.getWalletRepo( - chainStore.osmosis.chainName - ); - - if ( - !isOsmosisConnection && - osmosisWalletRepo.walletStatus !== WalletStatus.Connected - ) { - await osmosisWalletRepo - .connect(wallet.name, false) - .catch(handleConnectError); - } - - return walletRepo - .connect(wallet.name, false) - .then(async () => { - onConnectProp?.({ walletType: "cosmos" }); - - if (transaction1CTParams?.isOneClickEnabled) { - try { - await onCreate1CTSession({ walletRepo, transaction1CTParams }); - } catch (e) { - const error = e as CreateOneClickSessionError | Error; - - if (error instanceof Error) { - throw new CreateOneClickSessionError(error.message); - } - - throw e; - } - } - }) - .catch((e: Error | unknown) => { - if (e instanceof CreateOneClickSessionError) throw e; - handleConnectError( - e instanceof Error ? e : new Error("Unknown error.") - ); - }); - }; - - const onConnectWagmiWallet = async ({ - wallet, - chainId, - }: { - wallet: Connector; - chainId: EthereumChainIds | undefined; - }) => { - // Close modal to show WalletConnect QR code modal - if (wallet.type === WagmiWalletConnectType) { - onRequestClose(); - } - - return connectEvmWallet( - { connector: wallet, chainId: chainId }, - { - onSuccess: () => { - onConnectProp?.({ walletType: "evm" }); - }, - onError: (e) => { - console.error("Error while connecting to wallet. Details: ", e); - }, - } - ); - }; - - const onConnect: OnConnectWallet = async (param) => { - if (!param.wallet) return; - - if (param.walletType === "cosmos" && rootWalletRepo) { - return onConnectCosmosWallet({ - wallet: param.wallet, - walletRepo: rootWalletRepo, - }); - } - - if (param.walletType === "evm") { - return onConnectWagmiWallet({ - wallet: param.wallet, - chainId: param.chainId, - }).catch(noop); - } - }; - const onRequestBack = modalView !== "list" ? () => { @@ -389,7 +220,7 @@ export const WalletSelectModal: FunctionComponent = @@ -400,7 +231,7 @@ export const WalletSelectModal: FunctionComponent = connector={variables.connector as Connector} status={status} error={error} - onConnect={onConnect} + onConnect={onConnectWallet} /> )} @@ -426,7 +257,7 @@ export const WalletSelectModal: FunctionComponent = )} > = {...props} onRequestClose={onClose} modalView={modalView} - onConnect={onConnect} + onConnect={onConnectWallet} lazyWalletInfo={lazyWalletInfo} transaction1CTParams={transaction1CTParams} setTransaction1CTParams={setTransaction1CTParams} diff --git a/packages/web/modals/wallet-select/simple-wallet-list.tsx b/packages/web/modals/wallet-select/simple-wallet-list.tsx index 5b8790e1ca..09d89dc654 100644 --- a/packages/web/modals/wallet-select/simple-wallet-list.tsx +++ b/packages/web/modals/wallet-select/simple-wallet-list.tsx @@ -4,8 +4,8 @@ import React, { FunctionComponent, useMemo, useState } from "react"; import { SearchBox } from "~/components/input"; import { useTranslation, WalletSelectOption } from "~/hooks"; +import { OnConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; -import { OnConnectWallet } from "~/modals/wallet-select/utils"; export const SimpleWalletList: FunctionComponent<{ onConnect: OnConnectWallet; diff --git a/packages/web/modals/wallet-select/use-connect-wallet.ts b/packages/web/modals/wallet-select/use-connect-wallet.ts new file mode 100644 index 0000000000..606406d0ea --- /dev/null +++ b/packages/web/modals/wallet-select/use-connect-wallet.ts @@ -0,0 +1,214 @@ +import { ChainWalletBase, WalletRepo, WalletStatus } from "@cosmos-kit/core"; +import { + CosmosKitAccountsLocalStorageKey, + CosmosKitWalletLocalStorageKey, + CosmosRegistryWallet, +} from "@osmosis-labs/stores"; +import { noop } from "@osmosis-labs/utils"; +import { useState } from "react"; +import { Connector } from "wagmi"; + +import { EthereumChainIds } from "~/config/wagmi"; +import { CosmosWalletRegistry } from "~/config/wallet-registry"; +import { useConnectEvmWallet } from "~/hooks/evm-wallet"; +import { CreateOneClickSessionError } from "~/hooks/mutations/one-click-trading"; +import { WalletSelectOption } from "~/hooks/use-wallet-select"; +import { WagmiWalletConnectType } from "~/modals/wallet-select/use-selectable-wallets"; +import { useStore } from "~/stores"; + +export type OnConnectWallet = ( + params: + | { + walletType: "cosmos"; + wallet: CosmosRegistryWallet | ChainWalletBase | undefined; + } + | { + walletType: "evm"; + wallet: Connector; + chainId?: EthereumChainIds; + } +) => Promise; + +export const useConnectWallet = ({ + onRequestClose, + walletOptions, + onConnect: onConnectProp, + onConnecting, + onCreate1CTSession, + isOneClickEnabled, +}: { + onConnect?: (params: { walletType: "evm" | "cosmos" }) => void; + onConnecting?: () => void; + walletOptions: WalletSelectOption[]; + onRequestClose?: () => void; + + isOneClickEnabled?: boolean; + onCreate1CTSession?: (params: { walletRepo: WalletRepo }) => Promise; +}) => { + const { accountStore, chainStore } = useStore(); + + const { connectAsync: connectEvmWallet, ...connectEvmWalletUtils } = + useConnectEvmWallet(); + + const [lazyWalletInfo, setLazyWalletInfo] = + useState<(typeof CosmosWalletRegistry)[number]>(); + + const cosmosOption = walletOptions.find( + (option): option is Extract => + option.walletType === "cosmos" + ); + + const cosmosChainId = cosmosOption?.chainId; + const rootWalletRepo = cosmosChainId + ? accountStore.getWalletRepo(cosmosChainId) + : undefined; + const current = rootWalletRepo?.current; + const cosmosChainName = rootWalletRepo?.chainRecord.chain?.chain_name!; + + const onConnectCosmosWallet = async ({ + wallet, + walletRepo: walletRepoParam, + }: { + wallet: CosmosRegistryWallet | ChainWalletBase; + walletRepo: WalletRepo; + }) => { + if (current) { + await current?.disconnect(true); + } + + const handleConnectError = (e: Error) => { + console.error("Error while connecting to wallet. Details: ", e); + localStorage.removeItem(CosmosKitWalletLocalStorageKey); + localStorage.removeItem(CosmosKitAccountsLocalStorageKey); + }; + + if (!("lazyInstall" in wallet)) { + wallet + .connect(false) + .then(() => { + onConnectProp?.({ walletType: "cosmos" }); + }) + .catch(handleConnectError); + return; + } + + const isWalletInstalled = rootWalletRepo?.wallets.some( + ({ walletName }) => walletName === wallet.name + ); + + let walletRepo: WalletRepo; + + // if wallet is not installed, install it + if (!isWalletInstalled && "lazyInstall" in wallet) { + setLazyWalletInfo(wallet); + onConnecting?.(); + + // wallet is now walletInfo + const walletInfo = wallet; + const WalletClass = await wallet.lazyInstall(); + + const walletManager = await accountStore.addWallet( + new WalletClass(walletInfo) + ); + await walletManager.onMounted().catch(handleConnectError); + setLazyWalletInfo(undefined); + + walletRepo = walletManager.getWalletRepo(cosmosChainName!); + } else { + walletRepo = walletRepoParam; + } + + const isOsmosisConnection = + chainStore.osmosis.chainName === cosmosChainName!; + const osmosisWalletRepo = accountStore.getWalletRepo( + chainStore.osmosis.chainName + ); + + if ( + !isOsmosisConnection && + osmosisWalletRepo.walletStatus !== WalletStatus.Connected + ) { + await osmosisWalletRepo + .connect(wallet.name, false) + .catch(handleConnectError); + } + + return walletRepo + .connect(wallet.name, false) + .then(async () => { + onConnectProp?.({ walletType: "cosmos" }); + + if (isOneClickEnabled && onCreate1CTSession) { + try { + await onCreate1CTSession({ walletRepo }); + } catch (e) { + const error = e as CreateOneClickSessionError | Error; + + if (error instanceof Error) { + throw new CreateOneClickSessionError(error.message); + } + + throw e; + } + } + }) + .catch((e: Error | unknown) => { + if (e instanceof CreateOneClickSessionError) throw e; + handleConnectError( + e instanceof Error ? e : new Error("Unknown error.") + ); + }); + }; + + const onConnectWagmiWallet = async ({ + wallet, + chainId, + }: { + wallet: Connector; + chainId: EthereumChainIds | undefined; + }) => { + // Close modal to show WalletConnect QR code modal + if (wallet.type === WagmiWalletConnectType) { + onRequestClose?.(); + } + + await connectEvmWallet( + { connector: wallet, chainId: chainId }, + { + onSuccess: () => { + onConnectProp?.({ walletType: "evm" }); + }, + onError: (e) => { + console.error("Error while connecting to wallet. Details: ", e); + }, + } + ); + }; + + const onConnect: OnConnectWallet = async (param) => { + if (!param.wallet) return; + + if (param.walletType === "cosmos" && rootWalletRepo) { + return onConnectCosmosWallet({ + wallet: param.wallet, + walletRepo: rootWalletRepo, + }); + } + + if (param.walletType === "evm") { + return onConnectWagmiWallet({ + wallet: param.wallet, + chainId: param.chainId, + }).catch(noop); + } + }; + + return { + wagmi: connectEvmWalletUtils, + cosmos: { + lazyWalletInfo, + walletRepo: rootWalletRepo, + }, + onConnect, + }; +}; diff --git a/packages/web/modals/wallet-select/utils.ts b/packages/web/modals/wallet-select/utils.ts index a619ee547f..edf2225ea3 100644 --- a/packages/web/modals/wallet-select/utils.ts +++ b/packages/web/modals/wallet-select/utils.ts @@ -1,8 +1,4 @@ -import { ChainWalletBase, State, WalletStatus } from "@cosmos-kit/core"; -import { CosmosRegistryWallet } from "@osmosis-labs/stores"; -import { Connector } from "wagmi"; - -import { EthereumChainIds } from "~/config/wagmi"; +import { State, WalletStatus } from "@cosmos-kit/core"; export type ModalView = | "list" @@ -57,16 +53,3 @@ export function getModalView({ return "list"; } - -export type OnConnectWallet = ( - params: - | { - walletType: "cosmos"; - wallet: CosmosRegistryWallet | ChainWalletBase | undefined; - } - | { - walletType: "evm"; - wallet: Connector; - chainId?: EthereumChainIds; - } -) => void; diff --git a/packages/web/package.json b/packages/web/package.json index d72c62d565..4bd704e444 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -42,7 +42,7 @@ "@cosmos-kit/trust": "2.7.2", "@cosmos-kit/xdefi": "2.4.4", "@ethersproject/abi": "^5.7.0", - "@headlessui/react": "^1.7.8", + "@headlessui/react": "^2.1.1", "@keplr-wallet/common": "0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos": "0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/crypto": "0.12.48", @@ -138,8 +138,8 @@ "sharp": "^0.30.4", "tailwindcss-animate": "^1.0.7", "utility-types": "^3.10.0", - "viem": "2.13.3", - "wagmi": "^2.9.6", + "viem": "2.16.4", + "wagmi": "^2.10.8", "web3-utils": "^1.7.4", "zod": "^3.22.4" }, diff --git a/packages/web/pages/test-bridge.tsx b/packages/web/pages/test-bridge.tsx new file mode 100644 index 0000000000..d5ace6a003 --- /dev/null +++ b/packages/web/pages/test-bridge.tsx @@ -0,0 +1,37 @@ +import { NextPage } from "next"; +import Image from "next/image"; +import { NextSeo } from "next-seo"; +import { useMount } from "react-use"; + +import { useTranslation } from "~/hooks"; +import { useBridge } from "~/hooks/bridge"; + +const TestBridge: NextPage = () => { + const { t } = useTranslation(); + const { bridgeAsset } = useBridge(); + + useMount(() => { + bridgeAsset({ + direction: "deposit", + anyDenom: "USDC", + }); + }); + + return ( +
+ + {t("404.title")} +
{t("404.title")}
+
+ ); +}; + +export default TestBridge; diff --git a/packages/web/public/icons/sprite.svg b/packages/web/public/icons/sprite.svg index 352966e858..a865f1f497 100644 --- a/packages/web/public/icons/sprite.svg +++ b/packages/web/public/icons/sprite.svg @@ -1247,5 +1247,16 @@ fill="currentColor" /> + + + diff --git a/packages/web/server/api/local-router.ts b/packages/web/server/api/local-router.ts index f9647c7257..c978947a59 100644 --- a/packages/web/server/api/local-router.ts +++ b/packages/web/server/api/local-router.ts @@ -7,6 +7,8 @@ import { swapRouter, } from "@osmosis-labs/trpc"; +import { localBridgeTransferRouter } from "~/server/api/routers/local-bridge-transfer"; + /** * This section includes tRPC functions that execute on the client-side. * Caution: Ensure no sensitive data is exposed through these functions. */ @@ -16,4 +18,5 @@ export const localRouter = createTRPCRouter({ concentratedLiquidity: concentratedLiquidityRouter, oneClickTrading: oneClickTradingRouter, cms: cmsRouter, + bridgeTransfer: localBridgeTransferRouter, }); diff --git a/packages/web/server/api/routers/bridge-transfer.ts b/packages/web/server/api/routers/bridge-transfer.ts index 9054065234..3477100605 100644 --- a/packages/web/server/api/routers/bridge-transfer.ts +++ b/packages/web/server/api/routers/bridge-transfer.ts @@ -1,18 +1,21 @@ import { Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { Bridge, + BridgeChain, BridgeCoin, BridgeProviders, getBridgeExternalUrlSchema, getBridgeQuoteSchema, + getBridgeSupportedAssetsParams, } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY, getAssetPrice, + getChain, getTimeoutHeight, } from "@osmosis-labs/server"; import { createTRPCRouter, publicProcedure } from "@osmosis-labs/trpc"; -import { isNil, timeout } from "@osmosis-labs/utils"; +import { EthereumChainInfo, isNil, timeout } from "@osmosis-labs/utils"; import { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; import { z } from "zod"; @@ -198,6 +201,110 @@ export const bridgeTransferRouter = createTRPCRouter({ }; }), + getSupportedAssetsByBridge: publicProcedure + .input(getBridgeSupportedAssetsParams.extend({ bridge: z.string() })) + .query(async ({ input, ctx }) => { + const bridgeProviders = new BridgeProviders( + process.env.NEXT_PUBLIC_SQUID_INTEGRATOR_ID!, + { + ...ctx, + env: IS_TESTNET ? "testnet" : "mainnet", + cache: lruCache, + getTimeoutHeight: ({ destinationAddress }) => + getTimeoutHeight({ ...ctx, destinationAddress }), + } + ); + + const bridgeProvider = + bridgeProviders.bridges[ + input.bridge as keyof typeof bridgeProviders.bridges + ]; + + if (!bridgeProvider) { + throw new Error("Invalid bridge provider id: " + input.bridge); + } + + const supportedAssetFn = () => bridgeProvider.getSupportedAssets(input); + + /** If the bridge takes longer than 10 seconds to respond, we should timeout that query. */ + const supportedAssets = await timeout(supportedAssetFn, 10 * 1000)(); + + const assetsByChainId = supportedAssets.reduce< + Record< + BridgeChain["chainId"], + ((typeof supportedAssets)[number] & { providerName: string })[] + > + >((acc, asset) => { + if (!acc[asset.chainId]) { + acc[asset.chainId] = []; + } + + acc[asset.chainId].push({ ...asset, providerName: input.bridge }); + + return acc; + }, {}); + + const eventualChains = Array.from( + // Remove duplicate chains + new Map( + supportedAssets.map(({ chainId, chainType }) => [ + chainId, + { chainId, chainType }, + ]) + ).values() + ); + + const availableChains = eventualChains + .map(({ chainId, chainType }) => { + if (chainType === "evm") { + // TODO: Find a way to get eth chains from `getChain` function + const evmChain = Object.values(EthereumChainInfo).find( + (chain) => chain.id === chainId + ); + + if (!evmChain) { + return undefined; + } + + return { + prettyName: evmChain.name, + chainId: evmChain.id, + chainType, + }; + } else if (chainType === "cosmos") { + let cosmosChain: ReturnType | undefined; + try { + cosmosChain = getChain({ + chainList: ctx.chainList, + chainNameOrId: String(chainId), + }); + } catch {} + + if (!cosmosChain) { + return undefined; + } + + return { + prettyName: cosmosChain.chain_name, + chainId: cosmosChain.chain_id, + chainType, + }; + } + + return undefined; + }) + .filter((chain): chain is NonNullable => Boolean(chain)); + + return { + supportedAssets: { + providerName: bridgeProvider.providerName as Bridge, + inputAssetAddress: input.asset.address, + assetsByChainId, + availableChains, + }, + }; + }), + /** * Provide the transfer request for a given bridge transfer. */ diff --git a/packages/web/server/api/routers/local-bridge-transfer.ts b/packages/web/server/api/routers/local-bridge-transfer.ts new file mode 100644 index 0000000000..59902ae370 --- /dev/null +++ b/packages/web/server/api/routers/local-bridge-transfer.ts @@ -0,0 +1,217 @@ +import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; +import { + Bridge, + bridgeAssetSchema, + bridgeChainSchema, +} from "@osmosis-labs/bridge"; +import { + calcAssetValue, + captureErrorAndReturn, + DEFAULT_VS_CURRENCY, + getAsset, + getEvmBalance, + queryBalances, +} from "@osmosis-labs/server"; +import { + createTRPCRouter, + publicProcedure, + UserCosmosAddressSchema, + UserEvmAddressSchema, +} from "@osmosis-labs/trpc"; +import { getAddress } from "viem"; +import { z } from "zod"; + +export const localBridgeTransferRouter = createTRPCRouter({ + getSupportedAssetsBalances: publicProcedure + .input( + z.discriminatedUnion("type", [ + z + .object({ + type: z.literal("evm"), + assets: z.array( + bridgeChainSchema.and(bridgeAssetSchema).and( + z.object({ + supportedVariants: z.array(z.string()), + supportedProviders: z.array( + z.string().transform((v) => v as Bridge) + ), + }) + ) + ), + }) + .merge(UserEvmAddressSchema), + z + .object({ + type: z.literal("cosmos"), + assets: z.array( + bridgeChainSchema.and(bridgeAssetSchema).and( + z.object({ + supportedVariants: z.array(z.string()), + supportedProviders: z.array( + z.string().transform((v) => v as Bridge) + ), + }) + ) + ), + }) + .merge(UserCosmosAddressSchema), + ]) + ) + .query(async ({ input, ctx }) => { + if (input.type === "evm") { + return Promise.all( + input.assets + .filter( + (asset): asset is Extract => + asset.chainType !== "cosmos" + ) + .map(async (asset) => { + const emptyBalance = { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + + if (!input.userEvmAddress) return emptyBalance; + + const balance = await getEvmBalance({ + address: getAddress(asset.address), + userAddress: input.userEvmAddress, + chainId: asset.chainId, + }).catch(() => undefined); + + if (!balance) return emptyBalance; + + const decAmount = new Dec(balance.toString()); + /** + * Use the supported variant to determine the price of the ETH asset. + * This is because providers can return variant assets that are missing in + * our asset list. + * + * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. + */ + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: asset.supportedVariants[0], + amount: decAmount, + }).catch((e) => captureErrorAndReturn(e, undefined)); + + return { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + decAmount + ), + usdValue: new PricePretty( + DEFAULT_VS_CURRENCY, + usdValue ?? new Dec(0) + ), + }; + }) + ); + } + + if (input.type === "cosmos") { + if (!input.userCosmosAddress) { + return input.assets.map((asset) => ({ + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.denom, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + })); + } + + const assetsWithBalance = await Promise.all( + input.assets + .filter( + ( + asset + ): asset is Extract => + asset.chainType !== "evm" + ) + .map(async (asset) => { + const { balances } = await queryBalances({ + ...ctx, + chainId: asset.chainId, + bech32Address: input.userCosmosAddress!, + }); + + const balance = balances.find((a) => a.denom === asset.address); + + if (!balance) { + return { + ...asset, + amount: new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.denom, + }, + new Dec(0) + ), + usdValue: new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)), + }; + } + + const representativeAssetMinimalDenom = + asset.supportedVariants[0]; + const representativeAsset = getAsset({ + ...ctx, + anyDenom: representativeAssetMinimalDenom, + }); + + // is user asset, include user data + const usdValue = await calcAssetValue({ + ...ctx, + anyDenom: representativeAsset.coinMinimalDenom, + amount: balance.amount, + }).catch((e) => captureErrorAndReturn(e, undefined)); + + return { + ...asset, + amount: + new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(balance.amount) + ) ?? + new CoinPretty( + { + coinDecimals: asset.decimals, + coinDenom: asset.denom, + coinMinimalDenom: asset.address, + }, + new Dec(0) + ), + usdValue: new PricePretty( + DEFAULT_VS_CURRENCY, + usdValue ?? new Dec(0) + ), + }; + }) + ); + + return assetsWithBalance; + } + }), +}); diff --git a/packages/web/stores/assets/transfer-ui-config.ts b/packages/web/stores/assets/transfer-ui-config.ts index 1c6c534279..fe2688ff3d 100644 --- a/packages/web/stores/assets/transfer-ui-config.ts +++ b/packages/web/stores/assets/transfer-ui-config.ts @@ -1,7 +1,7 @@ import { KVStore } from "@keplr-wallet/common"; import { IBCCurrency } from "@keplr-wallet/types"; -import type { SourceChain } from "@osmosis-labs/bridge"; import { makeLocalStorageKVStore } from "@osmosis-labs/stores"; +import { AxelarSourceChain } from "@osmosis-labs/utils"; import { action, computed, @@ -174,7 +174,7 @@ export class ObservableTransferUIConfig { } if (balance.originBridgeInfo) { - const sourceChainKey: SourceChain = + const sourceChainKey: AxelarSourceChain = (await this.kvStore.get(makeAssetSrcNetworkPreferredKey(coinDenom))) || balance.originBridgeInfo?.defaultSourceChainId || balance.originBridgeInfo.sourceChainTokens[0].id; @@ -271,7 +271,7 @@ export class ObservableTransferUIConfig { onSelectAsset: ( denom: string, /** Is ibc transfer if `undefined`. */ - sourceChainKey?: SourceChain + sourceChainKey?: AxelarSourceChain ) => void ) { const availableAssets = this.assetsStore.ibcBalances.filter( @@ -289,7 +289,7 @@ export class ObservableTransferUIConfig { // override default source chain if prev selected by if (originBridgeInfo && defaultSourceChainId) originBridgeInfo.defaultSourceChainId = - (defaultSourceChainId as SourceChain) ?? undefined; + (defaultSourceChainId as AxelarSourceChain) ?? undefined; return { token: balance, @@ -322,7 +322,7 @@ export class ObservableTransferUIConfig { protected launchSelectAssetSourceModal( direction: TransferDir, balanceOnOsmosis: IBCBalance, - sourceChainKey: SourceChain + sourceChainKey: AxelarSourceChain ) { const wallets = this._ethClientWallets as ObservableWallet[]; const applicableWallets = wallets.filter(({ key }) => @@ -402,7 +402,7 @@ export class ObservableTransferUIConfig { direction: TransferDir, balanceOnOsmosis: IBCBalance, connectedWalletClient: ObservableWallet | undefined, - sourceChainKey: SourceChain, + sourceChainKey: AxelarSourceChain, onRequestSwitchWallet: () => void, onRequestBack?: () => void ) { diff --git a/packages/web/utils/ethereum.ts b/packages/web/utils/ethereum.ts new file mode 100644 index 0000000000..0c9733ac95 --- /dev/null +++ b/packages/web/utils/ethereum.ts @@ -0,0 +1,40 @@ +import { + ResourceUnavailableRpcError, + UnauthorizedProviderError, + UserRejectedRequestError, +} from "viem"; +import { BaseError } from "wagmi"; + +import { MultiLanguageT } from "~/hooks"; + +export function getWagmiToastErrorMessage({ + error, + t, + walletName, +}: { + error: BaseError; + t: MultiLanguageT; + walletName: string; +}) { + if (error.name === UserRejectedRequestError.name) { + return { + titleTranslationKey: "transactionFailed", + captionTranslationKey: "requestRejected", + }; + } else if (error.name === UnauthorizedProviderError.name) { + return { + titleTranslationKey: "Action Unavailable", + captionTranslationKey: "Please log into MetaMask", + }; + } else if (error.name === ResourceUnavailableRpcError.name) { + return { + titleTranslationKey: t("assets.transfer.errors.seeRequest", { + walletName, + }), + }; + } else { + return { + titleTranslationKey: "transactionFailed", + }; + } +} diff --git a/yarn.lock b/yarn.lock index 03f3c9c9fc..2f44bb1654 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,10 +2429,10 @@ long "^4.0.0" protobufjs "~6.11.2" -"@coinbase/wallet-sdk@4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.0.2.tgz#403b2194ecc9bcf8b8fd217ec5cd6529013b58f4" - integrity sha512-WMUeFbtS0rn8zavjAmNhFWq1r3TV7E5KuSij1Sar0/XuOC+nhj96uqSlIApAHdhuScoKZBq39VYsAQCHzOC6/w== +"@coinbase/wallet-sdk@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.0.4.tgz#634cd89bac93eeaf381a1f026476794e53431ed6" + integrity sha512-74c040CRnGhfRjr3ArnkAgud86erIqdkPHNt5HR1k9u97uTIZCJww9eGYT67Qf7gHPpGS/xW8Be1D4dvRm63FA== dependencies: buffer "^6.0.3" clsx "^1.2.1" @@ -4689,6 +4689,21 @@ dependencies: "@floating-ui/utils" "^0.2.1" +"@floating-ui/core@^1.6.0": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" + integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== + dependencies: + "@floating-ui/utils" "^0.2.4" + +"@floating-ui/dom@^1.0.0": + version "1.6.7" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015" + integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.4" + "@floating-ui/dom@^1.6.1": version "1.6.3" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" @@ -4704,11 +4719,32 @@ dependencies: "@floating-ui/dom" "^1.6.1" +"@floating-ui/react-dom@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" + integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/react@^0.26.16": + version "0.26.19" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.19.tgz#e3c713bec8a7264caa6f8195e0865f9210f483a1" + integrity sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA== + dependencies: + "@floating-ui/react-dom" "^2.1.1" + "@floating-ui/utils" "^0.2.4" + tabbable "^6.0.0" + "@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== +"@floating-ui/utils@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" + integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== + "@fractalwagmi/popup-connection@^1.0.18": version "1.1.1" resolved "https://registry.yarnpkg.com/@fractalwagmi/popup-connection/-/popup-connection-1.1.1.tgz#2dfff4f3bb89d17947adae597f355faf46c194a9" @@ -4755,13 +4791,15 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@headlessui/react@^1.7.8": - version "1.7.19" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" - integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== +"@headlessui/react@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.1.tgz#5403009bf7cd463a2deeec831c36eb235c32d008" + integrity sha512-808gVNUbRDbDR3GMNPHy+ON0uvR8b9H7IA+Q2UbhOsNCIjgwuwb2Iuv8VPT/1AW0UzLX8g10tN6LhF15GaUJCQ== dependencies: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" + "@floating-ui/react" "^0.26.16" + "@react-aria/focus" "^3.17.1" + "@react-aria/interactions" "^3.21.3" + "@tanstack/react-virtual" "3.5.0" "@humanwhocodes/config-array@^0.11.11": version "0.11.11" @@ -6322,10 +6360,10 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.0.0.tgz#8c2b9073fe0722d48693143b0dc8448840daa3bd" integrity sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ== -"@metamask/sdk-communication-layer@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.20.2.tgz#7f7fd334b2d26abd1a5a1ec1ffadf823a9589344" - integrity sha512-TN+whYbCClFSkx52Ild1RcjoRyz8YZgwNvZeooIcZIvCfBM6U9W5273KGiY7WLc/oO4KKmFk17d7vMO4gNvhhw== +"@metamask/sdk-communication-layer@0.26.2": + version "0.26.2" + resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.26.2.tgz#e36f298e78fa7c276a4db6a5bfa76085af3f75ef" + integrity sha512-YMqwjhCZ4sXYAsEp1LxLrZZycBwpUeEsA4yIx48m1yW9sZ8pv3NGnbjM+F0zf29DLjyqLxJdxHJ7b5YkgtB26g== dependencies: bufferutil "^4.0.8" date-fns "^2.29.3" @@ -6333,22 +6371,22 @@ utf-8-validate "^6.0.3" uuid "^8.3.2" -"@metamask/sdk-install-modal-web@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.20.2.tgz#1cf0eb3c26291de7598190878fa9a893c4eb2d66" - integrity sha512-0QiaZhV15AGdN1zU2jfTI32eC3YkwEpzDfR9+oiZ9bd2G72c6lYBhTsmDGUd01aP6A+bqJR5PjI8Wh2AWtoLeA== +"@metamask/sdk-install-modal-web@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.26.0.tgz#412a89747a96e94233eb59d2779ab26656096688" + integrity sha512-LyDQFIsWWyU0ZgZR3O9LzRqKzXcYUEGJRCNfb26IjFOquvmQosbhQV0jDNlVa8Tk2Fg4ykTPoaauANh6sVJYVQ== dependencies: qr-code-styling "^1.6.0-rc.1" -"@metamask/sdk@0.20.3": - version "0.20.3" - resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.20.3.tgz#73851d68ffe5d45c1872c024182922530b187b7a" - integrity sha512-HZ9NwA+LxiXzuy0YWbWsuD4xejQtp85bhcCAf8UgpA/0dOyF3RS4dKDdBBXSyRgk3RWPjeJgHxioaH4CmBmiRA== +"@metamask/sdk@0.26.3": + version "0.26.3" + resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.26.3.tgz#d7b08955f0b2fcb1248913a292490745d52fd57b" + integrity sha512-DM4BFPr1BDAIhTz7/RWb3oWQRvX79TJVZH8EL/Ljp+CRY7IjCbaVwaLdyQjVd8Doyq1V7AL4N/JjXplpo2YyYg== dependencies: "@metamask/onboarding" "^1.0.1" "@metamask/providers" "^15.0.0" - "@metamask/sdk-communication-layer" "0.20.2" - "@metamask/sdk-install-modal-web" "0.20.2" + "@metamask/sdk-communication-layer" "0.26.2" + "@metamask/sdk-install-modal-web" "0.26.0" "@types/dom-screen-wake-lock" "^1.0.0" bowser "^2.9.0" cross-fetch "^4.0.0" @@ -6806,7 +6844,7 @@ dependencies: "@noble/hashes" "1.3.3" -"@noble/curves@^1.0.0", "@noble/curves@^1.1.0", "@noble/curves@^1.2.0", "@noble/curves@^1.4.0", "@noble/curves@~1.4.0": +"@noble/curves@1.4.0", "@noble/curves@^1.0.0", "@noble/curves@^1.1.0", "@noble/curves@^1.2.0", "@noble/curves@^1.4.0", "@noble/curves@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== @@ -8005,6 +8043,45 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@react-aria/focus@^3.17.1": + version "3.17.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.17.1.tgz#c796a188120421e2fedf438cadacdf463c77ad29" + integrity sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ== + dependencies: + "@react-aria/interactions" "^3.21.3" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + +"@react-aria/interactions@^3.21.3": + version "3.21.3" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.21.3.tgz#a2a3e354a8b894bed7a46e1143453f397f2538d7" + integrity sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-aria/utils" "^3.24.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + +"@react-aria/ssr@^3.9.4": + version "3.9.4" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.4.tgz#9da8b10342c156e816dbfa4c9e713b21f274d7ab" + integrity sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-aria/utils@^3.24.1": + version "3.24.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.24.1.tgz#9d16023f07c23c41793c9030a9bd203a9c8cf0a7" + integrity sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q== + dependencies: + "@react-aria/ssr" "^3.9.4" + "@react-stately/utils" "^3.10.1" + "@react-types/shared" "^3.23.1" + "@swc/helpers" "^0.5.0" + clsx "^2.0.0" + "@react-native/normalize-color@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.1.0.tgz#939b87a9849e81687d3640c5efa2a486ac266f91" @@ -8056,6 +8133,18 @@ "@react-spring/shared" "~9.7.2" "@react-spring/types" "~9.7.2" +"@react-stately/utils@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.1.tgz#dc8685b4994bef0dc10c37b024074be8afbfba62" + integrity sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg== + dependencies: + "@swc/helpers" "^0.5.0" + +"@react-types/shared@^3.23.1": + version "3.23.1" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.23.1.tgz#2f23c81d819d0ef376df3cd4c944be4d6bce84c3" + integrity sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw== + "@reduxjs/toolkit@^1.9.1": version "1.9.7" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.7.tgz#7fc07c0b0ebec52043f8cb43510cf346405f78a6" @@ -8152,7 +8241,7 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" -"@scure/bip32@^1.3.0", "@scure/bip32@^1.3.1": +"@scure/bip32@1.4.0", "@scure/bip32@^1.3.0", "@scure/bip32@^1.3.1": version "1.4.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== @@ -8185,7 +8274,7 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" -"@scure/bip39@^1.2.0", "@scure/bip39@^1.2.1": +"@scure/bip39@1.3.0", "@scure/bip39@^1.2.0", "@scure/bip39@^1.2.1": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== @@ -9198,6 +9287,13 @@ dependencies: tslib "^2.4.0" +"@swc/helpers@^0.5.0": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7" + integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A== + dependencies: + tslib "^2.4.0" + "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -9253,12 +9349,12 @@ dependencies: "@tanstack/table-core" "8.10.3" -"@tanstack/react-virtual@^3.0.0-beta.60": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz#1ce466f530a10f781871360ed2bf7ff83e664f85" - integrity sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw== +"@tanstack/react-virtual@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.0.tgz#873b5b77cf78af563a4a11e6251ed51ee8868132" + integrity sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw== dependencies: - "@tanstack/virtual-core" "3.5.1" + "@tanstack/virtual-core" "3.5.0" "@tanstack/react-virtual@^3.0.0-beta.63": version "3.0.0-beta.63" @@ -9277,10 +9373,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.63.tgz#fa6ffc8b5b9bfef19006ea330f00456381c30361" integrity sha512-KhhfRYSoQpl0y+2axEw+PJZd/e/9p87PDpPompxcXnweNpt9ZHCT/HuNx7MKM9PVY/xzg9xJSWxwnSCrO+d6PQ== -"@tanstack/virtual-core@3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12" - integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ== +"@tanstack/virtual-core@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.0.tgz#108208d0f1d75271300bc5560cf9a85a1fa01e89" + integrity sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg== "@terra-money/feather.js@^1.0.8": version "1.0.11" @@ -10773,23 +10869,23 @@ abitype "0.8.7" eventemitter3 "^4.0.7" -"@wagmi/connectors@5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-5.0.5.tgz#040a4e2e6858d9d6dcf0c0f36d20cc07f8b49da1" - integrity sha512-EjMsmPeu4iYDSSfpvsCbpIwhns+E2FrMqujpcgqTboWkAeSoUEbhoAsSwmivMts+5XojOX8NTs6/KP4zQriolg== +"@wagmi/connectors@5.0.20": + version "5.0.20" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-5.0.20.tgz#01620a10d1d3fb15d7d9e06bb007f11c0dfdc7a5" + integrity sha512-H4jleQIiMFy1mUt+6vTtdL8Fw1yve1+b0c30RsfddgaoXjUaTHAeTUmDsGBPADRJfbg3pixLh6xt82z4Mab9hw== dependencies: - "@coinbase/wallet-sdk" "4.0.2" - "@metamask/sdk" "0.20.3" + "@coinbase/wallet-sdk" "4.0.4" + "@metamask/sdk" "0.26.3" "@safe-global/safe-apps-provider" "0.18.1" "@safe-global/safe-apps-sdk" "8.1.0" "@walletconnect/ethereum-provider" "2.13.0" "@walletconnect/modal" "2.6.2" cbw-sdk "npm:@coinbase/wallet-sdk@3.9.3" -"@wagmi/core@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.10.3.tgz#5184c94e2368b984d9ff8911bb83a3c9a92dc7d5" - integrity sha512-Sx5tWFzbLnwJk/aYPsaG8o4SQ8pVs5ucV5AVyPzA9Ibg3+J1P7qxOcfwPDXSNk67vmCGyZWlmBF/IwQChOJYbQ== +"@wagmi/core@2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.11.5.tgz#9fd47cfaeb26ae1546f705f871fcee44f0bd2b91" + integrity sha512-RmtZQkNf/ozdngyDST33WLTdKQHny9SsiNmxln8G06pbnOuhO4dDhnXnfiJ8Lh9GVIfFsjlmtqzfAIo1/86dqg== dependencies: eventemitter3 "5.0.1" mipd "0.0.5" @@ -11958,10 +12054,10 @@ abitype@0.9.8: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== -abitype@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" - integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== +abitype@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" + integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== abort-controller@^3.0.0: version "3.0.0" @@ -13741,7 +13837,7 @@ cli-width@^3.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -client-only@0.0.1, client-only@^0.0.1: +client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -13827,6 +13923,11 @@ clsx@^1.1.0, clsx@^1.2.1: resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + clsx@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" @@ -24969,6 +25070,11 @@ system-architecture@^0.1.0: resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + tabbable@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.0.1.tgz" @@ -26162,19 +26268,19 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" -viem@2.13.3: - version "2.13.3" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.3.tgz#950426e4cacf5e12fab2c202a339371901712481" - integrity sha512-3tlwDRKHSelupFjbFMdUxF41f79ktyH2F9PAQ9Dltbs1DpdDlR1x+Ksa0th6qkyjjAbpDZP3F5nMTJv/1GVPdQ== +viem@2.16.4: + version "2.16.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.16.4.tgz#5cc7bbed81ee815a131f509066972a8e849da801" + integrity sha512-5Dk8BUCUymVJRETeU4rNHhTIj+VtBEWBKU0veJ7URtLPltO8wY0/OaUQeN77OeMhmy/l1z0Gbrm9CEURgE06Iw== dependencies: "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "1.0.0" + "@noble/curves" "1.4.0" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + abitype "1.0.5" isows "1.0.4" - ws "8.13.0" + ws "8.17.1" viem@^1.0.0, viem@^1.1.4, viem@^1.20.3, viem@^1.6.0: version "1.21.4" @@ -26207,13 +26313,13 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" -wagmi@^2.9.6: - version "2.9.6" - resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.9.6.tgz#301557a7a6546f79fd14b8c40e878fa4242fa679" - integrity sha512-cRZJrI/N8XoPs5DwWP1JPaXPQnUKOr4q3w8xbbKSw2hv++4VXngyUn+clo5vqa/23AZTWwRo4vcJYJoHtHP9Hw== +wagmi@^2.10.8: + version "2.10.8" + resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.10.8.tgz#236e907e784eeae3dde808c400b62cb6b573fc51" + integrity sha512-25xJCTEQ3ug6tl86MnngzhXOJUo4tJufUUxlnb2qRz+aZFAcRGL+hhuBBZOJ552T49UPF0Hs9c6Rd4BKvwHLrg== dependencies: - "@wagmi/connectors" "5.0.5" - "@wagmi/core" "2.10.3" + "@wagmi/connectors" "5.0.20" + "@wagmi/core" "2.11.5" use-sync-external-store "1.2.0" walker@^1.0.8: @@ -26588,6 +26694,11 @@ ws@8.13.0, ws@^8.11.0: resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + ws@8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" From 147164fab58c8b7088b0e4c482a5a45577b0fca5 Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Mon, 1 Jul 2024 21:28:28 -0400 Subject: [PATCH 13/15] Jonator/pick jose withdraw (#3433) * feat: streamline chain and asset handling, and support withdrawal quotes * feat: handle withdraw wallets * feat: use getAddress to sanitize address * lint --------- Co-authored-by: Jose Felix --- packages/bridge/src/squid/index.ts | 5 +- .../amount-and-confirmation-screen.tsx | 68 ++- .../bridge/immersive/amount-screen.tsx | 524 +++++++++++------- .../immersive/bridge-network-select-modal.tsx | 83 +-- .../immersive/bridge-provider-dropdown.tsx | 82 ++- .../immersive/bridge-wallet-select-modal.tsx | 40 +- .../bridge/immersive/use-bridge-quote.ts | 85 ++- packages/web/localizations/de.json | 3 +- packages/web/localizations/en.json | 3 +- packages/web/localizations/es.json | 3 +- packages/web/localizations/fa.json | 3 +- packages/web/localizations/fr.json | 3 +- packages/web/localizations/gu.json | 3 +- packages/web/localizations/hi.json | 3 +- packages/web/localizations/ja.json | 3 +- packages/web/localizations/ko.json | 3 +- packages/web/localizations/pl.json | 3 +- packages/web/localizations/pt-br.json | 3 +- packages/web/localizations/ro.json | 3 +- packages/web/localizations/ru.json | 3 +- packages/web/localizations/tr.json | 3 +- packages/web/localizations/zh-cn.json | 3 +- packages/web/localizations/zh-hk.json | 3 +- packages/web/localizations/zh-tw.json | 3 +- 24 files changed, 570 insertions(+), 368 deletions(-) diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 28026d090d..3625487ac8 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -156,7 +156,10 @@ export class SquidBridgeProvider implements BridgeProvider { }); } - if (data.route.params.toToken.address !== toAsset.address) { + if ( + data.route.params.toToken.address.toLowerCase() !== + toAsset.address.toLowerCase() + ) { throw new BridgeQuoteError({ bridgeId: SquidBridgeProvider.ID, errorType: "UnsupportedQuoteError", diff --git a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx index 49e1ede849..67b20ce27a 100644 --- a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx @@ -1,9 +1,9 @@ import { CoinPretty } from "@keplr-wallet/unit"; import { BridgeChain } from "@osmosis-labs/bridge"; -import { MinimalAsset } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { useState } from "react"; +import { getAddress } from "viem"; import { AmountScreen } from "~/components/bridge/immersive/amount-screen"; import { ImmersiveBridgeScreens } from "~/components/bridge/immersive/immersive-bridge"; @@ -35,7 +35,7 @@ export const AmountAndConfirmationScreen = observer( const { accountStore } = useStore(); const [sourceAsset, setSourceAsset] = useState(); - const [destinationAsset, setDestinationAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); @@ -43,41 +43,57 @@ export const AmountAndConfirmationScreen = observer( const [fiatAmount, setFiatAmount] = useState("0"); // Wallets - const destinationAccount = accountStore.getWallet( - accountStore.osmosisChainId - ); const { address: evmAddress } = useEvmWalletAccount(); - const sourceChain = direction === "deposit" ? fromChain : toChain; - const destinationChain = direction === "deposit" ? toChain : fromChain; - - const cosmosCounterpartyAccount = - sourceChain?.chainType === "evm" || isNil(sourceChain) + const fromChainCosmosAccount = + fromChain?.chainType === "evm" || isNil(fromChain) ? undefined - : accountStore.getWallet(sourceChain.chainId); + : accountStore.getWallet(fromChain.chainId); - const sourceAddress = - sourceChain?.chainType === "evm" - ? evmAddress - : cosmosCounterpartyAccount?.address; + const toChainCosmosAccount = + toChain?.chainType === "evm" || isNil(toChain) + ? undefined + : accountStore.getWallet(toChain.chainId); const quote = useBridgeQuote({ - destinationAddress: destinationAccount?.address, - destinationChain, - destinationAsset: destinationAsset + toAddress: + toChain?.chainType === "evm" + ? evmAddress + : toChainCosmosAccount?.address, + toChain: toChain, + toAsset: destinationAsset + ? { + address: + toChain?.chainType === "evm" + ? getAddress(destinationAsset.address) + : destinationAsset.address, + decimals: destinationAsset.decimals, + denom: destinationAsset.denom, + } + : undefined, + fromAddress: + fromChain?.chainType === "evm" + ? evmAddress + : fromChainCosmosAccount?.address, + fromChain: fromChain, + fromAsset: sourceAsset ? { - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, + address: + fromChain?.chainType === "evm" + ? getAddress(sourceAsset.address) + : sourceAsset.address, + decimals: sourceAsset.decimals, + denom: sourceAsset.denom, + amount: sourceAsset.amount, } : undefined, - sourceAddress, - sourceChain, - sourceAsset, direction, onRequestClose: onClose, inputAmount: cryptoAmount, - bridges: sourceAsset?.supportedProviders, + bridges: + direction === "deposit" + ? sourceAsset?.supportedProviders + : destinationAsset?.supportedProviders, onTransfer: () => { setCryptoAmount("0"); setFiatAmount("0"); @@ -93,8 +109,6 @@ export const AmountAndConfirmationScreen = observer( void; toChain: BridgeChain | undefined; @@ -69,8 +65,8 @@ interface AmountScreenProps { sourceAsset: SupportedAssetWithAmount | undefined; setSourceAsset: (asset: SupportedAssetWithAmount | undefined) => void; - destinationAsset: MinimalAsset | undefined; - setDestinationAsset: (asset: MinimalAsset | undefined) => void; + destinationAsset: SupportedAsset | undefined; + setDestinationAsset: (asset: SupportedAsset | undefined) => void; cryptoAmount: string; fiatAmount: string; @@ -85,8 +81,6 @@ export const AmountScreen = observer( direction, selectedDenom, - sourceChain, - fromChain, setFromChain, toChain, @@ -105,6 +99,7 @@ export const AmountScreen = observer( quote, }: AmountScreenProps) => { + const { setCurrentScreen } = useScreenManager(); const { accountStore } = useStore(); const { onOpenWalletSelect } = useWalletSelect(); const { t } = useTranslation(); @@ -144,28 +139,35 @@ export const AmountScreen = observer( } = useDisclosure(); // Wallets - const destinationAccount = accountStore.getWallet( - accountStore.osmosisChainId - ); + const osmosisAccount = accountStore.getWallet(accountStore.osmosisChainId); const { address: evmAddress, connector: evmConnector, isConnected: isEvmWalletConnected, } = useEvmWalletAccount(); - const cosmosCounterpartyAccountRepo = - sourceChain?.chainType === "evm" || isNil(sourceChain) + const fromCosmosCounterpartyAccountRepo = + fromChain?.chainType === "evm" || isNil(fromChain) + ? undefined + : accountStore.getWalletRepo(fromChain.chainId); + const fromCosmosCounterpartyAccount = + fromChain?.chainType === "evm" || isNil(fromChain) + ? undefined + : accountStore.getWallet(fromChain.chainId); + + const toCosmosCounterpartyAccountRepo = + toChain?.chainType === "evm" || isNil(toChain) ? undefined - : accountStore.getWalletRepo(sourceChain.chainId); - const cosmosCounterpartyAccount = - sourceChain?.chainType === "evm" || isNil(sourceChain) + : accountStore.getWalletRepo(toChain.chainId); + const toCosmosCounterpartyAccount = + toChain?.chainType === "evm" || isNil(toChain) ? undefined - : accountStore.getWallet(sourceChain.chainId); + : accountStore.getWallet(toChain.chainId); - const sourceAddress = - sourceChain?.chainType === "evm" + const toAddress = + toChain?.chainType === "evm" ? evmAddress - : cosmosCounterpartyAccount?.address; + : toCosmosCounterpartyAccount?.address; const { data: assetsInOsmosis } = api.edge.assets.getCanonicalAssetWithVariants.useQuery( @@ -199,17 +201,48 @@ export const AmountScreen = observer( canonicalAsset ); - const { supportedAssetsByChainId, supportedChains } = - useBridgesSupportedAssets({ - assets: assetsInOsmosis, - chain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - }); + const { + supportedAssetsByChainId: counterpartySupportedAssetsByChainId, + supportedChains, + } = useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); + + const supportedSourceAssets: SupportedAsset[] | undefined = useMemo(() => { + if (!fromChain) return undefined; - const supportedAssets = - supportedAssetsByChainId[sourceChain?.chainId ?? ""]; + // Use Osmosis Assets to get the source asset + if (direction === "withdraw") { + const selectedAsset = assetsInOsmosis?.find( + (asset) => asset.coinDenom === selectedDenom + ); + if (!selectedAsset) return undefined; + return [ + { + address: selectedAsset.coinMinimalDenom, + decimals: selectedAsset.coinDecimals, + chainId: fromChain.chainId, + chainType: fromChain.chainType, + denom: selectedAsset.coinDenom, + // Providers are not needed for withdrawals; they will be derived from the destinationAsset + supportedProviders: [], + supportedVariants: [selectedAsset.coinMinimalDenom], + }, + ]; + } + + return counterpartySupportedAssetsByChainId[fromChain.chainId]; + }, [ + assetsInOsmosis, + counterpartySupportedAssetsByChainId, + direction, + fromChain, + selectedDenom, + ]); const supportedChainsAsBridgeChain = useMemo( () => @@ -244,74 +277,119 @@ export const AmountScreen = observer( const hasMoreThanOneChainType = !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); - const { - data: sourceAssetsBalances, - isLoading: isLoadingSourceAssetsBalance, - } = api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( - sourceChain?.chainType === "evm" - ? { - type: "evm", - assets: supportedAssets as Extract< - SupportedAsset, - { chainType: "evm" } - >[], - userEvmAddress: evmAddress, - } - : { - type: "cosmos", - assets: supportedAssets as Extract< - SupportedAsset, - { chainType: "cosmos" } - >[], - userCosmosAddress: cosmosCounterpartyAccount?.address, - }, - { - enabled: !isNil(sourceChain) && !isNil(supportedAssets), - - select: (data) => { - let nextData: typeof data = data; - - // Filter out assets with no balance - if (nextData) { - const filteredData = nextData.filter((asset) => - asset.amount.toDec().gt(new Dec(0)) - ); - - // If there are no assets with balance, leave one to be selected - if (filteredData.length === 0) { - nextData = [nextData[0]]; - } else { - nextData = filteredData; + const { data: assetsBalances, isLoading: isLoadingAssetsBalance } = + api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( + fromChain?.chainType === "evm" + ? { + type: "evm", + assets: supportedSourceAssets as Extract< + SupportedAsset, + { chainType: "evm" } + >[], + userEvmAddress: evmAddress, + } + : { + type: "cosmos", + assets: supportedSourceAssets as Extract< + SupportedAsset, + { chainType: "cosmos" } + >[], + userCosmosAddress: fromCosmosCounterpartyAccount?.address, + }, + { + enabled: !isNil(fromChain) && !isNil(supportedSourceAssets), + + select: (data) => { + let nextData: typeof data = data; + + // Filter out assets with no balance + if (nextData) { + const filteredData = nextData.filter((asset) => + asset.amount.toDec().gt(new Dec(0)) + ); + + // If there are no assets with balance, leave one to be selected + if (filteredData.length === 0) { + nextData = [nextData[0]]; + } else { + nextData = filteredData; + } } - } - if (!sourceAsset && nextData) { - const highestBalance = nextData.reduce( - (acc, curr) => - curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, - nextData[0] - ); + if (!sourceAsset && nextData) { + const highestBalance = nextData.reduce( + (acc, curr) => + curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, + nextData[0] + ); - setSourceAsset(highestBalance); - } + setSourceAsset(highestBalance); + } - return nextData; - }, - } - ); + return nextData; + }, + } + ); /** + * Deposit * Set the initial destination asset based on the source asset. */ useEffect(() => { - if (!isNil(sourceAsset) && !isNil(assetsInOsmosis)) { + if ( + direction === "deposit" && + !isNil(sourceAsset) && + !isNil(assetsInOsmosis) && + isNil(destinationAsset) + ) { const destinationAsset = assetsInOsmosis.find( (a) => a.coinMinimalDenom === sourceAsset.supportedVariants[0] )!; - setDestinationAsset(destinationAsset); + setDestinationAsset({ + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + denom: destinationAsset.coinDenom, + supportedProviders: sourceAsset.supportedProviders, + supportedVariants: [destinationAsset.coinMinimalDenom], + }); } - }, [assetsInOsmosis, setDestinationAsset, sourceAsset]); + }, [ + accountStore.osmosisChainId, + assetsInOsmosis, + destinationAsset, + direction, + setDestinationAsset, + sourceAsset, + ]); + + /** + * Withdraw + * Set the initial destination asset based on the source asset. + */ + useEffect(() => { + if ( + direction === "withdraw" && + isNil(destinationAsset) && + counterpartySupportedAssetsByChainId && + toChain + ) { + const counterpartyAssets = + counterpartySupportedAssetsByChainId[toChain.chainId]; + + if (counterpartyAssets && counterpartyAssets.length > 0) { + setDestinationAsset(counterpartyAssets[0]); + } + } + }, [ + counterpartySupportedAssetsByChainId, + destinationAsset, + direction, + setDestinationAsset, + toChain, + ]); /** * Set the osmosis chain based on the direction @@ -363,40 +441,48 @@ export const AmountScreen = observer( useEffect(() => { if (!fromChain || !toChain) return; + const account = + direction === "deposit" + ? fromCosmosCounterpartyAccount + : toCosmosCounterpartyAccount; + const accountRepo = + direction === "deposit" + ? fromCosmosCounterpartyAccountRepo + : toCosmosCounterpartyAccountRepo; const chain = direction === "deposit" ? fromChain : toChain; if ( // If the chain is an EVM chain, we don't need to connect the cosmos chain chain.chainType !== "cosmos" || // Or if the account is already connected - !!cosmosCounterpartyAccount?.address || + !!account?.address || // Or if there's no available cosmos chain !firstSupportedCosmosChain || // Or if the account is already connected - !!cosmosCounterpartyAccountRepo?.current + !!accountRepo?.current ) { return; } - cosmosCounterpartyAccountRepo - ?.connect(destinationAccount?.walletName) - .catch(() => - // Display the connect modal if the user for some reason rejects the connection - onOpenWalletSelect({ - walletOptions: [ - { walletType: "cosmos", chainId: String(chain.chainId) }, - ], - }) - ); + accountRepo?.connect(osmosisAccount?.walletName).catch(() => + // Display the connect modal if the user for some reason rejects the connection + onOpenWalletSelect({ + walletOptions: [ + { walletType: "cosmos", chainId: String(chain.chainId) }, + ], + }) + ); }, [ - destinationAccount?.walletName, - cosmosCounterpartyAccount?.address, - cosmosCounterpartyAccountRepo, direction, firstSupportedCosmosChain, fromChain, + fromCosmosCounterpartyAccount, + fromCosmosCounterpartyAccountRepo, onOpenWalletSelect, + osmosisAccount?.walletName, toChain, + toCosmosCounterpartyAccount, + toCosmosCounterpartyAccountRepo, ]); /** @@ -431,7 +517,7 @@ export const AmountScreen = observer( if ( isLoadingCanonicalAssetPrice || - isNil(supportedAssets) || + isNil(supportedSourceAssets) || !assetsInOsmosis || !canonicalAsset || !destinationAsset || @@ -521,13 +607,18 @@ export const AmountScreen = observer( ? t("transfer.deposit") : t("transfer.withdraw")}
{" "} - token image{" "} - {canonicalAsset.coinDenom} +
@@ -577,19 +668,16 @@ export const AmountScreen = observer(
{inputUnit === "fiat" ? ( - <> - - + ) : (

@@ -641,7 +729,19 @@ export const AmountScreen = observer( -

@@ -649,63 +749,61 @@ export const AmountScreen = observer(
<> - {isLoadingSourceAssetsBalance && ( + {isLoadingAssetsBalance && (

Looking for balances

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

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

-
- )} + {!isLoadingAssetsBalance && assetsBalances?.length === 1 && ( +
+

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

+
+ )} - {!isLoadingSourceAssetsBalance && - (sourceAssetsBalances?.length ?? 0) > 1 && ( -
- {(sourceAssetsBalances ?? []).map((asset) => { - const isActive = - asset.amount.currency.coinMinimalDenom === - sourceAsset?.address; - return ( - - ); - })} -
- )} + {!isLoadingAssetsBalance && (assetsBalances?.length ?? 0) > 1 && ( +
+ {(assetsBalances ?? []).map((asset) => { + const isActive = + asset.amount.currency.coinMinimalDenom === + sourceAsset?.address; + return ( + + ); + })} +
+ )} {walletConnected && ( @@ -724,14 +822,16 @@ export const AmountScreen = observer( { + const chain = + direction === "deposit" ? fromChain : toChain; + return chain?.chainType === "evm" + ? chain + : firstSupportedEvmChain; + })()} + cosmosChain={(() => { + const chain = + direction === "deposit" ? fromChain : toChain; + return chain?.chainType === "cosmos" + ? chain + : firstSupportedCosmosChain; + })()} /> ) : ( @@ -775,14 +879,14 @@ export const AmountScreen = observer(
@@ -818,7 +922,7 @@ export const AmountScreen = observer(
- {destinationAsset?.coinDenom} + {destinationAsset?.denom} {sourceAsset.supportedVariants.map( (variantCoinMinimalDenom, index) => { + // TODO: HANDLE WITHDRAW CASE const asset = assetsInOsmosis.find( (asset) => asset.coinMinimalDenom === variantCoinMinimalDenom )!; const onClick = () => { - setDestinationAsset(asset); + setDestinationAsset({ + chainType: "cosmos", + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + chainId: accountStore.osmosisChainId, + denom: asset.coinDenom, + supportedProviders: sourceAsset.supportedProviders, + supportedVariants: [asset.coinMinimalDenom], + }); }; // Show all as 'deposit as' for now @@ -852,7 +965,7 @@ export const AmountScreen = observer( false ?? asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = - destinationAsset?.coinDenom === asset.coinDenom; + destinationAsset?.denom === asset.coinDenom; const isCanonicalAsset = index === 0; @@ -1067,7 +1180,7 @@ export const AmountScreen = observer( @@ -1075,7 +1188,7 @@ export const AmountScreen = observer( isNil(selectedQuote.gasCost) ? (
@@ -1165,9 +1278,16 @@ export const AmountScreen = observer( disabled={ !isNil(buttonErrorMessage) || isLoadingBridgeQuote || - isLoadingBridgeTransaction + isLoadingBridgeTransaction || + cryptoAmount === "" || + cryptoAmount === "0" } className="w-full text-h6 font-h6" + variant={ + warnUserOfSlippage || warnUserOfPriceImpact + ? "destructive" + : "default" + } > {buttonText} @@ -1185,14 +1305,10 @@ export const AmountScreen = observer( direction={direction} isOpen={areMoreOptionsVisible} fromAsset={sourceAsset} - toAsset={{ - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, - }} + toAsset={destinationAsset} fromChain={fromChain} toChain={toChain} - toAddress={sourceAddress} + toAddress={toAddress} onRequestClose={() => setAreMoreOptionsVisible(false)} /> diff --git a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index 76967e323e..e5c39f03e8 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -126,50 +126,57 @@ export const BridgeNetworkSelectModal = ({ size="full" />
- {filteredChains.map((chain) => ( - - ))} + } as BridgeChain); + }} + > + {chain.prettyName} + {shouldSwitchChain && ( + + {t("transfer.connect")} + + )} + + ); + })}
diff --git a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx index 072cdbc84e..c92ed2bee9 100644 --- a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx +++ b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx @@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Dec, PricePretty } from "@keplr-wallet/unit"; import { Bridge } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import { isNil } from "@osmosis-labs/utils"; import classNames from "classnames"; import Image from "next/image"; import { useMemo } from "react"; @@ -24,16 +25,38 @@ export const BridgeProviderDropdown = ({ onSelect, }: Props) => { const { t } = useTranslation(); - const fastestQuote = useMemo( - () => - quotes.reduce((prev, current) => - prev.data.estimatedTime.asMilliseconds() < - current.data.estimatedTime.asMilliseconds() - ? prev - : current - ), - [quotes] - ); + const fastestQuote = useMemo(() => { + const minTime = Math.min( + ...quotes.map((q) => q.data.estimatedTime.asMilliseconds()) + ); + const uniqueFastestQuotes = quotes.filter( + (q) => q.data.estimatedTime.asMilliseconds() === minTime + ); + return uniqueFastestQuotes.length === 1 + ? uniqueFastestQuotes[0] + : undefined; + }, [quotes]); + + const cheapestQuote = useMemo(() => { + const minFee = quotes + .map((q) => q.data.transferFeeFiat?.toDec() ?? new Dec(0)) + .reduce((acc, fee) => { + if (acc === null || fee.lt(acc)) { + return fee; + } + return acc; + }, null as Dec | null); + + const uniqueCheapestQuotes = quotes.filter((q) => { + const feeDec = q.data.transferFeeFiat?.toDec(); + return !isNil(feeDec) && !isNil(minFee) && feeDec.equals(minFee); + }); + + return uniqueCheapestQuotes.length === 1 + ? uniqueCheapestQuotes[0] + : undefined; + }, [quotes]); + return ( {({ open }) => ( @@ -63,18 +86,15 @@ export const BridgeProviderDropdown = ({ className="z-[1000] mt-3 flex max-h-64 flex-col gap-1 overflow-auto rounded-2xl bg-osmoverse-825 px-2 py-2" > {quotes.map( - ( - { - data: { - provider, - estimatedTime, - transferFeeFiat, - gasCostFiat, - expectedOutputFiat, - }, + ({ + data: { + provider, + estimatedTime, + transferFeeFiat, + gasCostFiat, + expectedOutputFiat, }, - index - ) => { + }) => { const totalFee = transferFeeFiat ?.add( gasCostFiat ?? @@ -82,6 +102,11 @@ export const BridgeProviderDropdown = ({ ) .toString(); const isSelected = selectedQuote.provider.id === provider.id; + const isCheapest = + cheapestQuote?.data.provider.id === provider.id; + const isFastest = + fastestQuote?.data.provider.id === provider.id; + return (
diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx index fa9856d5e8..c7950b7fc2 100644 --- a/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx @@ -8,10 +8,12 @@ import { Connector } from "wagmi"; import { SearchBox } from "~/components/input"; import { Button } from "~/components/ui/button"; +import { SwitchingNetworkState } from "~/components/wallet-states/switching-network-state"; import { EthereumChainIds } from "~/config/wagmi"; import { useDisconnectEvmWallet, useEvmWalletAccount, + useSwitchEvmChain, } from "~/hooks/evm-wallet"; import { ModalBase, ModalBaseProps } from "~/modals"; import { EvmWalletState } from "~/modals/wallet-select/evm-wallet-state"; @@ -71,9 +73,14 @@ export const BridgeWalletSelectScreen = ({ const [search, setSearch] = useState(""); const [isManaging, setIsManaging] = useState(false); + const [isSwitchingChain, setIsSwitchingChain] = useState(false); - const { connector: evmConnector, isConnected: isEvmWalletConnected } = - useEvmWalletAccount(); + const { + connector: evmConnector, + isConnected: isEvmWalletConnected, + chainId: currentEvmChainId, + } = useEvmWalletAccount(); + const { switchChainAsync } = useSwitchEvmChain(); const { disconnect: disconnectEvmWallet } = useDisconnectEvmWallet(); @@ -122,6 +129,17 @@ export const BridgeWalletSelectScreen = ({ ); } + if (isEvmWalletConnected && isSwitchingChain) { + return ( +
+ +
+ ); + } + const showEvmWallets = !isNil(evmChain) && !isNil(evmWallets); return ( @@ -176,7 +194,23 @@ export const BridgeWalletSelectScreen = ({ )} {isEvmWalletConnected && !isNil(evmChain) && ( { + onClick={async () => { + const shouldSwitchChain = + isEvmWalletConnected && + currentEvmChainId !== evmChain.chainId; + + if (shouldSwitchChain) { + try { + setIsSwitchingChain(true); + await switchChainAsync({ + chainId: evmChain.chainId as EthereumChainIds, + }); + } catch { + setIsSwitchingChain(false); + return; + } + } + onSelectChain(evmChain); onClose(); }} diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index 362530dcc2..6ec7d177eb 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -31,13 +31,13 @@ export const useBridgeQuote = ({ inputAmount: inputAmountRaw, - sourceAddress, - sourceChain, - sourceAsset, + fromAddress, + fromChain, + fromAsset, - destinationAddress, - destinationAsset, - destinationChain, + toAddress, + toAsset, + toChain, bridges = ["Axelar", "Skip", "Squid", "IBC"], @@ -48,13 +48,13 @@ export const useBridgeQuote = ({ inputAmount: string; - sourceAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; - sourceChain: BridgeChain | undefined; - sourceAddress: string | undefined; + fromAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; + fromChain: BridgeChain | undefined; + fromAddress: string | undefined; - destinationAsset: BridgeAsset | undefined; - destinationChain: BridgeChain | undefined; - destinationAddress: string | undefined; + toAsset: BridgeAsset | undefined; + toChain: BridgeChain | undefined; + toAddress: string | undefined; bridges?: Bridge[]; @@ -72,19 +72,6 @@ export const useBridgeQuote = ({ useSendEvmTransaction(); const { t } = useTranslation(); - // In the context of Osmosis, this refers to the Osmosis chain. - const destinationPath = { - address: destinationAddress, - asset: destinationAsset, - chain: destinationChain, - }; - - const sourcePath = { - address: sourceAddress, - asset: sourceAsset, - chain: sourceChain, - }; - const isDeposit = direction === "deposit"; const isWithdraw = direction === "withdraw"; @@ -93,14 +80,17 @@ export const useBridgeQuote = ({ RouterInputs["bridgeTransfer"]["getQuoteByBridge"], "bridge" | "fromAmount" > - > = { - fromAddress: isDeposit ? sourcePath.address : destinationPath.address, - fromAsset: isDeposit ? sourcePath.asset : destinationPath.asset, - fromChain: isDeposit ? sourcePath.chain : destinationPath.chain, - toAddress: isDeposit ? destinationPath.address : sourcePath.address, - toAsset: isDeposit ? destinationPath.asset : sourcePath.asset, - toChain: isDeposit ? destinationPath.chain : sourcePath.chain, - }; + > = useMemo( + () => ({ + fromAddress, + fromAsset, + fromChain, + toAddress, + toAsset, + toChain, + }), + [fromAddress, fromAsset, fromChain, toAddress, toAsset, toChain] + ); const [selectedBridgeProvider, setSelectedBridgeProvider] = useState(null); @@ -129,10 +119,10 @@ export const useBridgeQuote = ({ debouncedInputValue === "" ? "0" : debouncedInputValue ).mul( // CoinPretty only accepts whole amounts - DecUtils.getTenExponentNInPrecisionRange(destinationAsset?.decimals ?? 0) + DecUtils.getTenExponentNInPrecisionRange(toAsset?.decimals ?? 0) ); - const availableBalance = sourceAsset?.amount; + const availableBalance = fromAsset?.amount; const isInsufficientBal = inputAmountRaw !== "" && @@ -396,13 +386,13 @@ export const useBridgeQuote = ({ .trim(true) .toString(), isWithdraw, - destinationAddress ?? "" // use osmosis account (destinationAddress) for account keys (vs any EVM account) + toAddress ?? "" // use osmosis account (destinationAddress) for account keys (vs any EVM account) ); } }, [ availableBalance, - destinationAddress, + toAddress, inputAmount, inputAmountRaw, isWithdraw, @@ -413,10 +403,9 @@ export const useBridgeQuote = ({ const [isApprovingToken, setIsApprovingToken] = useState(false); const isSendTxPending = (() => { - if (!destinationChain) return false; - return destinationChain.chainType === "cosmos" - ? accountStore.getWallet(destinationChain.chainId)?.txTypeInProgress !== - "" + if (!toChain) return false; + return toChain.chainType === "cosmos" + ? accountStore.getWallet(toChain.chainId)?.txTypeInProgress !== "" : isEthTxPending; })(); @@ -503,13 +492,13 @@ export const useBridgeQuote = ({ const handleCosmosTx = async ( quote: NonNullable["quote"] ) => { - if (!destinationChain || destinationChain?.chainType !== "cosmos") { + if (!toChain || toChain?.chainType !== "cosmos") { throw new Error("Destination chain is not cosmos"); } const transactionRequest = quote.transactionRequest as CosmosBridgeTransactionRequest; return accountStore.signAndBroadcast( - destinationChain.chainId, + toChain.chainId, transactionRequest.msgTypeUrl, [ { @@ -522,12 +511,12 @@ export const useBridgeQuote = ({ undefined, (tx: DeliverTxResponse) => { if (tx.code == null || tx.code === 0) { - const queries = queriesStore.get(destinationChain.chainId); + const queries = queriesStore.get(toChain.chainId); // After succeeding to send token, refresh the balance. const queryBalance = queries.queryBalances // If we get here destination address is defined - .getQueryBech32Address(destinationAddress!) + .getQueryBech32Address(toAddress!) .balances.find((bal) => { return ( bal.currency.coinMinimalDenom === @@ -576,12 +565,12 @@ export const useBridgeQuote = ({ const warnUserOfSlippage = selectedQuote?.isSlippageTooHigh; const warnUserOfPriceImpact = selectedQuote?.isPriceImpactTooHigh; const isCorrectEvmChainSelected = - sourceChain?.chainType === "evm" - ? currentEvmChainId === sourceChain?.chainId + fromChain?.chainType === "evm" + ? currentEvmChainId === fromChain?.chainId : true; let buttonErrorMessage: string | undefined; - if (!sourceAddress) { + if (!fromAddress) { buttonErrorMessage = t("assets.transfer.errors.missingAddress"); } else if (hasNoQuotes) { buttonErrorMessage = t("assets.transfer.errors.noQuotesAvailable"); diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 274e04b7ed..197a70356d 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -968,7 +968,8 @@ "totalFees": "Gesamtkosten", "estimatedAmountReceived": "Geschätzter erhaltener Betrag", "slippageWarning": "Der erwartete Ausgabebetrag für diese Transaktion ist erheblich niedriger als der Eingabebetrag. Daher kann es sein, dass Sie einen anderen Endbetrag als erwartet erhalten.", - "priceImpactWarning": "Die Preiseinflüsse dieser Transaktion betragen {priceImpact} und können sich auf den endgültigen Betrag auswirken." + "priceImpactWarning": "Die Preiseinflüsse dieser Transaktion betragen {priceImpact} und können sich auf den endgültigen Betrag auswirken.", + "connect": "Verbinden" }, "unknownError": "Unbekannter Fehler", "viewExplorer": "Explorer anzeigen", diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 7051643f84..3985ed58cb 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -968,7 +968,8 @@ "totalFees": "Total fees", "estimatedAmountReceived": "Estimated amount received", "slippageWarning": "The expected output for this transaction is significantly lower than the input amount, which may result in receiving a different final amount than expected.", - "priceImpactWarning": "The price impact of this transaction is {priceImpact}, which may influence the final amount received." + "priceImpactWarning": "The price impact of this transaction is {priceImpact}, which may influence the final amount received.", + "connect": "Connect" }, "unknownError": "Unknown error", "viewExplorer": "View explorer", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 65225fea8a..2d67542419 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -968,7 +968,8 @@ "totalFees": "Tarifas totales", "estimatedAmountReceived": "Cantidad estimada recibida", "slippageWarning": "El resultado esperado para esta transacción es significativamente menor que el monto de entrada, lo que puede resultar en recibir un monto final diferente al esperado.", - "priceImpactWarning": "El impacto en el precio de esta transacción es {priceImpact} , lo que puede influir en el importe final recibido." + "priceImpactWarning": "El impacto en el precio de esta transacción es {priceImpact} , lo que puede influir en el importe final recibido.", + "connect": "Conectar" }, "unknownError": "Error desconocido", "viewExplorer": "Ver Explorador", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index b42d816d69..1138116875 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -968,7 +968,8 @@ "totalFees": "مجموع هزینه ها", "estimatedAmountReceived": "مبلغ تخمینی دریافت شده", "slippageWarning": "خروجی مورد انتظار برای این تراکنش به طور قابل توجهی کمتر از مقدار ورودی است که ممکن است منجر به دریافت مبلغ نهایی متفاوتی نسبت به انتظار شود.", - "priceImpactWarning": "تأثیر قیمت این تراکنش {priceImpact} است که ممکن است بر مبلغ نهایی دریافتی تأثیر بگذارد." + "priceImpactWarning": "تأثیر قیمت این تراکنش {priceImpact} است که ممکن است بر مبلغ نهایی دریافتی تأثیر بگذارد.", + "connect": "اتصال" }, "unknownError": "خطای نا شناس", "viewExplorer": "مشاهده جزئیات تراکنش", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index fd1762d19b..059d7aaf96 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -968,7 +968,8 @@ "totalFees": "Total des frais", "estimatedAmountReceived": "Montant estimé reçu", "slippageWarning": "Le résultat attendu pour cette transaction est nettement inférieur au montant d'entrée, ce qui peut entraîner la réception d'un montant final différent de celui attendu.", - "priceImpactWarning": "L'impact sur le prix de cette transaction est {priceImpact} , ce qui peut influencer le montant final reçu." + "priceImpactWarning": "L'impact sur le prix de cette transaction est {priceImpact} , ce qui peut influencer le montant final reçu.", + "connect": "Connecter" }, "unknownError": "Erreur inconnue", "viewExplorer": "Voir dans l'exploreur", diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 3fb49f2b09..a3ca46d00c 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -968,7 +968,8 @@ "totalFees": "કુલ ફી", "estimatedAmountReceived": "પ્રાપ્ત થયેલ અંદાજિત રકમ", "slippageWarning": "આ ટ્રાન્ઝેક્શન માટે અપેક્ષિત આઉટપુટ ઇનપુટ રકમ કરતાં નોંધપાત્ર રીતે ઓછું છે, જેના પરિણામે અપેક્ષા કરતાં અલગ અંતિમ રકમ પ્રાપ્ત થઈ શકે છે.", - "priceImpactWarning": "આ વ્યવહારની કિંમતની અસર {priceImpact} છે, જે પ્રાપ્ત થયેલી અંતિમ રકમને પ્રભાવિત કરી શકે છે." + "priceImpactWarning": "આ વ્યવહારની કિંમતની અસર {priceImpact} છે, જે પ્રાપ્ત થયેલી અંતિમ રકમને પ્રભાવિત કરી શકે છે.", + "connect": "જોડાવા" }, "unknownError": "અજાણી ભૂલ", "viewExplorer": "સંશોધક જુઓ", diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index 9a50dbead1..62b86fa14c 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -968,7 +968,8 @@ "totalFees": "कुल शुल्क", "estimatedAmountReceived": "अनुमानित प्राप्त राशि", "slippageWarning": "इस लेनदेन के लिए अपेक्षित आउटपुट इनपुट राशि से काफी कम है, जिसके परिणामस्वरूप अपेक्षित राशि से भिन्न अंतिम राशि प्राप्त हो सकती है।", - "priceImpactWarning": "इस लेनदेन का मूल्य प्रभाव {priceImpact} है, जो प्राप्त अंतिम राशि को प्रभावित कर सकता है।" + "priceImpactWarning": "इस लेनदेन का मूल्य प्रभाव {priceImpact} है, जो प्राप्त अंतिम राशि को प्रभावित कर सकता है।", + "connect": "जोड़ना" }, "unknownError": "अज्ञात त्रुटि", "viewExplorer": "एक्सप्लोरर देखें", diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index 130c525a7c..c37d9190a6 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -968,7 +968,8 @@ "totalFees": "合計料金", "estimatedAmountReceived": "受け取る推定金額", "slippageWarning": "このトランザクションの予想出力は入力金額よりも大幅に低いため、予想とは異なる最終金額を受け取る可能性があります。", - "priceImpactWarning": "この取引の価格影響は{priceImpact}であり、最終的に受け取る金額に影響する可能性があります。" + "priceImpactWarning": "この取引の価格影響は{priceImpact}であり、最終的に受け取る金額に影響する可能性があります。", + "connect": "接続する" }, "unknownError": "不明なエラー", "viewExplorer": "エクスプローラーを表示する", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 5a7bd3ffae..50cb876078 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -968,7 +968,8 @@ "totalFees": "총 수수료", "estimatedAmountReceived": "예상 수령 금액", "slippageWarning": "이 거래에 대한 예상 출력은 입력 금액보다 상당히 낮으므로 예상과 다른 최종 금액을 받게 될 수 있습니다.", - "priceImpactWarning": "이 거래의 가격 영향은 {priceImpact} 이며, 최종 수령 금액에 영향을 미칠 수 있습니다." + "priceImpactWarning": "이 거래의 가격 영향은 {priceImpact} 이며, 최종 수령 금액에 영향을 미칠 수 있습니다.", + "connect": "연결하다" }, "unknownError": "알 수 없는 에러", "viewExplorer": "블록 익스플로러 보기", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index 79d29e2c07..ac146b9216 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -968,7 +968,8 @@ "totalFees": "Wszystkie koszty", "estimatedAmountReceived": "Szacunkowa otrzymana kwota", "slippageWarning": "Oczekiwany wynik dla tej transakcji jest znacznie niższy niż kwota wejściowa, co może skutkować otrzymaniem kwoty końcowej innej niż oczekiwano.", - "priceImpactWarning": "Wpływ cenowy tej transakcji to {priceImpact} , co może mieć wpływ na ostateczną otrzymaną kwotę." + "priceImpactWarning": "Wpływ cenowy tej transakcji to {priceImpact} , co może mieć wpływ na ostateczną otrzymaną kwotę.", + "connect": "Łączyć" }, "unknownError": "Nieznany błąd", "viewExplorer": "zobacz eksplorer", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index d4a0f78309..c72ab9cfa7 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -968,7 +968,8 @@ "totalFees": "Taxas totais", "estimatedAmountReceived": "Valor estimado recebido", "slippageWarning": "O resultado esperado para esta transação é significativamente inferior ao valor do insumo, o que pode resultar no recebimento de um valor final diferente do esperado.", - "priceImpactWarning": "O impacto no preço desta transação é {priceImpact} , o que pode influenciar o valor final recebido." + "priceImpactWarning": "O impacto no preço desta transação é {priceImpact} , o que pode influenciar o valor final recebido.", + "connect": "Conectar" }, "unknownError": "Erro desconhecido", "viewExplorer": "Visualizar explorer", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 0628a369b7..52b6095f53 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -968,7 +968,8 @@ "totalFees": "Taxele totale", "estimatedAmountReceived": "Suma estimată primită", "slippageWarning": "Ieșirea așteptată pentru această tranzacție este semnificativ mai mică decât suma de intrare, ceea ce poate duce la primirea unei alte sume finale decât cele așteptate.", - "priceImpactWarning": "Impactul asupra prețului acestei tranzacții este {priceImpact} , care poate influența suma finală primită." + "priceImpactWarning": "Impactul asupra prețului acestei tranzacții este {priceImpact} , care poate influența suma finală primită.", + "connect": "Conectați" }, "unknownError": "Eroare necunoscuta", "viewExplorer": "vezi explorer", diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 76a2729f38..3938f99817 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -968,7 +968,8 @@ "totalFees": "Общая сумма сборов", "estimatedAmountReceived": "Ориентировочная полученная сумма", "slippageWarning": "Ожидаемый результат этой транзакции значительно ниже входной суммы, что может привести к получению другой конечной суммы, чем ожидалось.", - "priceImpactWarning": "Влияние этой транзакции на цену составляет {priceImpact} , что может повлиять на конечную полученную сумму." + "priceImpactWarning": "Влияние этой транзакции на цену составляет {priceImpact} , что может повлиять на конечную полученную сумму.", + "connect": "Соединять" }, "unknownError": "Неизвестная ошибка", "viewExplorer": "Посмотреть проводник", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 36d18c4961..77a46b2c8e 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -968,7 +968,8 @@ "totalFees": "Toplam ücretler", "estimatedAmountReceived": "Alınan tahmini miktar", "slippageWarning": "Bu işlem için beklenen çıktı, girdi tutarından önemli ölçüde düşüktür ve bu, beklenenden farklı bir nihai tutarın alınmasıyla sonuçlanabilir.", - "priceImpactWarning": "Bu işlemin fiyat etkisi {priceImpact} olup, alınan son tutarı etkileyebilir." + "priceImpactWarning": "Bu işlemin fiyat etkisi {priceImpact} olup, alınan son tutarı etkileyebilir.", + "connect": "Bağlamak" }, "unknownError": "Bilinmeyen hata", "viewExplorer": "gezginde görüntüle", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index dd366acfb6..01f5b40b91 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -968,7 +968,8 @@ "totalFees": "总费用", "estimatedAmountReceived": "预计收到金额", "slippageWarning": "该交易的预期输出明显低于输入金额,这可能导致收到的最终金额与预期不同。", - "priceImpactWarning": "此交易的价格影响为{priceImpact} ,可能会影响最终收到的金额。" + "priceImpactWarning": "此交易的价格影响为{priceImpact} ,可能会影响最终收到的金额。", + "connect": "连接" }, "unknownError": "未知错误", "viewExplorer": "浏览器查看", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 9f3181cd71..2fb92a90cd 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -968,7 +968,8 @@ "totalFees": "總費用", "estimatedAmountReceived": "預計收到金額", "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", - "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。", + "connect": "連接" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index c9b03c3b24..10c724b78f 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -968,7 +968,8 @@ "totalFees": "總費用", "estimatedAmountReceived": "預計收到金額", "slippageWarning": "該交易的預期輸出明顯低於輸入金額,這可能導致收到與預期不同的最終金額。", - "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。" + "priceImpactWarning": "此交易的價格影響為{priceImpact} ,這可能會影響最終收到的金額。", + "connect": "連接" }, "unknownError": "未知錯誤", "viewExplorer": "使用區塊瀏覽器查看", From 7303a82adee4452bf43d932d25053257af2fe828 Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Tue, 2 Jul 2024 09:12:58 +0200 Subject: [PATCH 14/15] fix: :bug: fix default platforms (#3428) --- .../server/src/queries/complex/earn/strategies.ts | 3 +++ packages/web/hooks/use-get-earn-strategies.ts | 1 + packages/web/pages/earn/index.tsx | 15 ++++++++------- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/server/src/queries/complex/earn/strategies.ts b/packages/server/src/queries/complex/earn/strategies.ts index 2d25afb447..417f6ba358 100644 --- a/packages/server/src/queries/complex/earn/strategies.ts +++ b/packages/server/src/queries/complex/earn/strategies.ts @@ -114,12 +114,14 @@ export async function getStrategies({ getFreshValue: async (): Promise<{ riskReportUrl?: string; categories: StategyCMSCategory[]; + platforms: StategyCMSCategory[]; strategies: StrategyCMSData[]; }> => { try { const cmsData = await queryOsmosisCMS<{ strategies: RawStrategyCMSData[]; categories: StategyCMSCategory[]; + platforms: StategyCMSCategory[]; riskReportUrl: string; }>({ filePath: `cms/earn/strategies.json` }); @@ -172,6 +174,7 @@ export async function getStrategies({ return { riskReportUrl: cmsData.riskReportUrl, categories: cmsData.categories, + platforms: cmsData.platforms, strategies: aggregatedStrategies.filter((strat) => !strat.unlisted), }; } catch (error) { diff --git a/packages/web/hooks/use-get-earn-strategies.ts b/packages/web/hooks/use-get-earn-strategies.ts index 40b06a0d4b..3bd69e98d8 100644 --- a/packages/web/hooks/use-get-earn-strategies.ts +++ b/packages/web/hooks/use-get-earn-strategies.ts @@ -214,6 +214,7 @@ export const useGetEarnStrategies = ( return { strategies, + cmsData, ...additionalBalanceData, holdenDenoms, areBalancesLoading, diff --git a/packages/web/pages/earn/index.tsx b/packages/web/pages/earn/index.tsx index 06164085c8..ff7e8539eb 100644 --- a/packages/web/pages/earn/index.tsx +++ b/packages/web/pages/earn/index.tsx @@ -53,6 +53,7 @@ function Earn() { useNavBar({ title: t("earnPage.title") }); const { + cmsData, strategies, myStrategies, totalBalance, @@ -78,18 +79,18 @@ function Earn() { { label: "Perps LP", value: "Perps LP" }, { label: "Lending", value: "Lending" }, ], - platform: [ - { label: "Osmosis", value: "Osmosis" }, - { label: "Quasar", value: "Quasar" }, - { label: "Levana", value: "Levana" }, - { label: "Mars", value: "Mars" }, - ], + platform: cmsData?.platforms + ? cmsData.platforms.map((platform) => ({ + label: platform.name, + value: platform.name, + })) + : [], lockDurationType: "all", search: "", specialTokens: [], rewardType: "all", }), - [holdenDenoms?.length, isWalletConnected] + [holdenDenoms?.length, cmsData, isWalletConnected] ); useEffect(() => { From 93d75b9d099bc00e17a43460f5105aac3ffbc2aa Mon Sep 17 00:00:00 2001 From: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:46:50 +0200 Subject: [PATCH 15/15] Always run delete-deployments for Synt Monitoring (#3436) * Always run delete-deployments for Synt Monitoring * fixed portfolio tests --- .github/workflows/monitoring-e2e-tests.yml | 3 ++- .../web/e2e/tests/portfolio.wallet.spec.ts | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/monitoring-e2e-tests.yml b/.github/workflows/monitoring-e2e-tests.yml index 69e67b9b75..f02df28070 100644 --- a/.github/workflows/monitoring-e2e-tests.yml +++ b/.github/workflows/monitoring-e2e-tests.yml @@ -207,7 +207,8 @@ jobs: delete-deployments: runs-on: ubuntu-latest - needs: frontend-e2e-tests + if: always() + needs: [frontend-e2e-tests, fe-quote-tests] steps: - name: Delete Previous deployments uses: actions/github-script@v7 diff --git a/packages/web/e2e/tests/portfolio.wallet.spec.ts b/packages/web/e2e/tests/portfolio.wallet.spec.ts index 6e3b94f933..54884d2f39 100644 --- a/packages/web/e2e/tests/portfolio.wallet.spec.ts +++ b/packages/web/e2e/tests/portfolio.wallet.spec.ts @@ -8,11 +8,11 @@ import { WalletPage } from "../pages/wallet-page"; test.describe("Test Portfolio feature", () => { let context: BrowserContext; - const privateKey = - process.env.PRIVATE_KEY ?? - "0x9866a7f11faf50d5ccc36b0ce57e6fa72c5032367c44279b9ca829713c78bab8"; + const privateKey = process.env.PRIVATE_KEY ?? "private_key"; const password = process.env.PASSWORD ?? "TestPassword2024."; let portfolioPage: PortfolioPage; + let dollarBalanceRegEx = /\$\d+/; + let digitBalanceRegEx = /\d+\.\d+/; test.beforeAll(async () => { console.log("\nBefore test setup Wallet Extension."); @@ -52,27 +52,28 @@ test.describe("Test Portfolio feature", () => { test("User should be able to see native balances", async () => { const osmoBalance = await portfolioPage.getBalanceFor("OSMO"); - expect(osmoBalance).toMatch(/\$\d+\.\d+/); + expect(osmoBalance).toMatch(dollarBalanceRegEx); const atomBalance = await portfolioPage.getBalanceFor("ATOM"); - expect(atomBalance).toMatch(/\$\d+\.\d+/); + expect(atomBalance).toMatch(dollarBalanceRegEx); const usdtBalance = await portfolioPage.getBalanceFor("USDT"); - expect(usdtBalance).toMatch(/\$\d+\.\d+/); + expect(usdtBalance).toMatch(dollarBalanceRegEx); const usdcBalance = await portfolioPage.getBalanceFor("USDC"); - expect(usdcBalance).toMatch(/\$\d+\.\d+/); + expect(usdcBalance).toMatch(dollarBalanceRegEx); const tiaBalance = await portfolioPage.getBalanceFor("TIA"); - expect(tiaBalance).toMatch(/\$\d+\.\d+/); + expect(tiaBalance).toMatch(dollarBalanceRegEx); const daiBalance = await portfolioPage.getBalanceFor("DAI"); - expect(daiBalance).toMatch(/\$\d+\.\d/); + expect(daiBalance).toMatch(dollarBalanceRegEx); }); test("User should be able to see bridged balances", async () => { const injBalance = await portfolioPage.getBalanceFor("INJ"); - expect(injBalance).toMatch(/\$\d+\.\d+/); + expect(injBalance).toMatch(dollarBalanceRegEx); const ethBalance = await portfolioPage.getBalanceFor("ETH"); - expect(ethBalance).toMatch(/\$\d+\.\d+/); + expect(ethBalance).toMatch(dollarBalanceRegEx); const kujiBalance = await portfolioPage.getBalanceFor("KUJI"); - expect(kujiBalance).toMatch(/\$\d+\.\d/); + expect(kujiBalance).toMatch(dollarBalanceRegEx); const abtcBalance = await portfolioPage.getBalanceFor("allBTC"); - expect(abtcBalance).toMatch(/\d+\.\d+/); + // allBTC has not $ price atm + expect(abtcBalance).toMatch(digitBalanceRegEx); }); });