diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx index 5794e666ce..e61c9ec1ab 100644 --- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -40,6 +40,7 @@ export interface CurrencyInputPanelProps extends Partial { tokenSelectorDisabled?: boolean displayTokenName?: boolean displayChainName?: boolean + hideReceiveAmounts?: boolean inputTooltip?: string showSetMax?: boolean maxBalance?: CurrencyAmount | undefined @@ -79,6 +80,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps): ReactNode { tokenSelectorDisabled = false, displayTokenName = false, displayChainName = false, + hideReceiveAmounts, inputTooltip, onUserInput, allowsOffchainSigning, @@ -277,7 +279,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps): ReactNode { - {receiveAmountInfo && currency && ( + {receiveAmountInfo && currency && !hideReceiveAmounts && ( { setIsBridgingEnabled(shouldEnableBridging) diff --git a/apps/cowswap-frontend/src/entities/bridgeOrders/state/bridgeOrdersStateSerializer.ts b/apps/cowswap-frontend/src/entities/bridgeOrders/state/bridgeOrdersStateSerializer.ts index 4f565a5916..ebc20b1e81 100644 --- a/apps/cowswap-frontend/src/entities/bridgeOrders/state/bridgeOrdersStateSerializer.ts +++ b/apps/cowswap-frontend/src/entities/bridgeOrders/state/bridgeOrdersStateSerializer.ts @@ -38,6 +38,12 @@ export function serializeQuoteAmounts(amounts: BridgeQuoteAmounts): BridgeQuoteA swapMinReceiveAmount: serializeAmount(amounts.swapMinReceiveAmount), bridgeMinReceiveAmount: serializeAmount(amounts.bridgeMinReceiveAmount), bridgeFee: serializeAmount(amounts.bridgeFee), + bridgeFeeAmounts: amounts.bridgeFeeAmounts + ? { + amountInDestinationCurrency: serializeAmount(amounts.bridgeFeeAmounts.amountInDestinationCurrency), + amountInIntermediateCurrency: serializeAmount(amounts.bridgeFeeAmounts.amountInIntermediateCurrency), + } + : undefined, } } @@ -48,6 +54,12 @@ export function deserializeQuoteAmounts(amounts: BridgeQuoteAmounts, provider: DefaultBridgeProvider, flag: boolean): void { + if (flag) { + providers.add(provider) + } else { + providers.delete(provider) + } +} + +export function BridgeProvidersUpdater(): null { + const setBridgeProviders = useSetAtom(bridgeProvidersAtom) + const { isNearIntentsBridgeProviderEnabled, isAcrossBridgeProviderEnabled, isBungeeBridgeProviderEnabled } = + useFeatureFlags() + + useEffect(() => { + // Skip updating till all flags are loaded + if ( + [isNearIntentsBridgeProviderEnabled, isAcrossBridgeProviderEnabled, isBungeeBridgeProviderEnabled].some( + (v) => typeof v !== 'boolean', + ) + ) { + return + } + + setBridgeProviders((providers) => { + const newProviders = new Set(providers) + + toggleProvider(newProviders, bungeeBridgeProvider, isBungeeBridgeProviderEnabled) + toggleProvider(newProviders, nearIntentsBridgeProvider, isNearIntentsBridgeProviderEnabled) + toggleProvider(newProviders, acrossBridgeProvider, isAcrossBridgeProviderEnabled) + + bridgingSdk.setAvailableProviders([...newProviders].map((p) => p.info.dappId)) + + return newProviders + }) + }, [ + isNearIntentsBridgeProviderEnabled, + isAcrossBridgeProviderEnabled, + isBungeeBridgeProviderEnabled, + setBridgeProviders, + ]) + + return null +} diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/bridgeProvidersAtom.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/bridgeProvidersAtom.ts index acc65089c4..e36a9df008 100644 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/bridgeProvidersAtom.ts +++ b/apps/cowswap-frontend/src/entities/bridgeProvider/bridgeProvidersAtom.ts @@ -1,7 +1,7 @@ import { atom } from 'jotai' -import { isProd } from '@cowprotocol/common-utils' +import { DefaultBridgeProvider } from '@cowprotocol/sdk-bridging' -import { bridgeProviders } from 'tradingSdk/bridgingSdk' +import { bungeeBridgeProvider } from 'tradingSdk/bridgingSdk' -export const bridgeProvidersAtom = atom(isProd ? [bridgeProviders[0]] : bridgeProviders) +export const bridgeProvidersAtom = atom(new Set([bungeeBridgeProvider])) diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/index.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/index.ts index e2d444e702..d27704e917 100644 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/index.ts +++ b/apps/cowswap-frontend/src/entities/bridgeProvider/index.ts @@ -1,2 +1,3 @@ export { useBridgeSupportedNetworks, useBridgeSupportedNetwork } from './useBridgeSupportedNetworks' export { useBridgeSupportedTokens } from './useBridgeSupportedTokens' +export { BridgeProvidersUpdater } from './BridgeProvidersUpdater' diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProviders.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProviders.ts deleted file mode 100644 index c39536aab7..0000000000 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProviders.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useAtomValue } from 'jotai' - -import { BridgeProvider, BridgeQuoteResult } from '@cowprotocol/sdk-bridging' - -import { bridgeProvidersAtom } from './bridgeProvidersAtom' - -export function useBridgeProviders(): BridgeProvider[] { - return useAtomValue(bridgeProvidersAtom) -} diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProvidersIds.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProvidersIds.ts new file mode 100644 index 0000000000..68e49fb461 --- /dev/null +++ b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeProvidersIds.ts @@ -0,0 +1,9 @@ +import { useAtomValue } from 'jotai' + +import { bridgeProvidersAtom } from './bridgeProvidersAtom' + +export function useBridgeProvidersIds(): string[] { + const providers = useAtomValue(bridgeProvidersAtom) + + return [...providers].map((p) => p.info.dappId) +} diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx deleted file mode 100644 index 996fa40159..0000000000 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.test.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { ReactNode } from 'react' - -import { ALL_SUPPORTED_CHAINS, SupportedChainId } from '@cowprotocol/cow-sdk' -import { BridgeProvider, BridgeQuoteResult } from '@cowprotocol/sdk-bridging' -import { ALL_SUPPORTED_CHAINS_MAP } from '@cowprotocol/sdk-config' - -import { renderHook, waitFor } from '@testing-library/react' -import { SWRConfig } from 'swr' - -import { useBridgeProviders } from './useBridgeProviders' -import { useBridgeSupportedNetwork, useBridgeSupportedNetworks } from './useBridgeSupportedNetworks' - -jest.mock('./useBridgeProviders', () => ({ - useBridgeProviders: jest.fn(), -})) - -const mockUseBridgeProviders = useBridgeProviders as jest.MockedFunction - -// Mock console.error to avoid noise in tests -jest.spyOn(console, 'error').mockImplementation(() => {}) - -const chains = [...ALL_SUPPORTED_CHAINS].sort((a, b) => a.id - b.id) - -const mockProvider1: jest.Mocked> = { - info: { dappId: 'provider1', name: 'Provider 1', logoUrl: '', website: '' }, - getBuyTokens: jest.fn(), - getNetworks: jest.fn(), - getIntermediateTokens: jest.fn(), - getQuote: jest.fn(), - getUnsignedBridgeCall: jest.fn(), - getGasLimitEstimationForHook: jest.fn(), - getSignedHook: jest.fn(), - decodeBridgeHook: jest.fn(), - getBridgingParams: jest.fn(), - getExplorerUrl: jest.fn(), - getStatus: jest.fn(), - getCancelBridgingTx: jest.fn(), - getRefundBridgingTx: jest.fn(), -} - -const mockProvider2: jest.Mocked> = { - info: { dappId: 'provider2', name: 'Provider 2', logoUrl: '', website: '' }, - getBuyTokens: jest.fn(), - getNetworks: jest.fn(), - getIntermediateTokens: jest.fn(), - getQuote: jest.fn(), - getUnsignedBridgeCall: jest.fn(), - getGasLimitEstimationForHook: jest.fn(), - getSignedHook: jest.fn(), - decodeBridgeHook: jest.fn(), - getBridgingParams: jest.fn(), - getExplorerUrl: jest.fn(), - getStatus: jest.fn(), - getCancelBridgingTx: jest.fn(), - getRefundBridgingTx: jest.fn(), -} - -const wrapper = ({ children }: { children: React.ReactNode }): ReactNode => { - return new Map() }}>{children} -} - -describe('useBridgeSupportedNetworks', () => { - beforeEach(() => { - jest.clearAllMocks() - mockUseBridgeProviders.mockReturnValue([mockProvider1, mockProvider2]) - }) - - afterEach(() => { - jest.clearAllTimers() - }) - - it('should fetch and combine networks from multiple providers', async () => { - mockProvider1.getNetworks.mockResolvedValue([chains[0], chains[1]]) - mockProvider2.getNetworks.mockResolvedValue([chains[1], chains[2]]) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toHaveLength(3) - expect(result.current.isLoading).toBe(false) - }) - - expect(mockProvider1.getNetworks).toHaveBeenCalledTimes(1) - expect(mockProvider2.getNetworks).toHaveBeenCalledTimes(1) - - const networks = result.current.data! - expect(networks.find((n) => n.id === 1)).toBeDefined() - expect(networks.find((n) => n.id === 56)).toBeDefined() - expect(networks.find((n) => n.id === 100)).toBeDefined() - }) - - it('should deduplicate networks by id', async () => { - const duplicateNetwork = chains[0] - - mockProvider1.getNetworks.mockResolvedValue([duplicateNetwork]) - mockProvider2.getNetworks.mockResolvedValue([duplicateNetwork]) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toHaveLength(1) - expect(result.current.data![0]).toEqual(duplicateNetwork) - }) - }) - - it('should handle provider failures gracefully', async () => { - mockProvider1.getNetworks.mockRejectedValue(new Error('Provider 1 failed')) - mockProvider2.getNetworks.mockResolvedValue([chains[0]]) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toHaveLength(1) - expect(result.current.data![0]).toEqual(chains[0]) - }) - }) - - it('should return empty array when all providers fail', async () => { - mockProvider1.getNetworks.mockRejectedValue(new Error('Provider 1 failed')) - mockProvider2.getNetworks.mockRejectedValue(new Error('Provider 2 failed')) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toEqual([]) - }) - }) - - it('should create proper cache key with provider ids', () => { - const { result, rerender } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - // Change providers to trigger re-fetch - const newProvider = { ...mockProvider1, info: { ...mockProvider1.info, dappId: 'provider3' } } - mockUseBridgeProviders.mockReturnValue([newProvider]) - - rerender() - - expect(result.current.isValidating || result.current.isLoading).toBe(true) - }) - - it('should handle empty network arrays from providers', async () => { - mockProvider1.getNetworks.mockResolvedValue([]) - mockProvider2.getNetworks.mockResolvedValue([]) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toEqual([]) - }) - }) - - it('should maintain network object integrity', async () => { - const networkWithExtraProps = { - ...chains[0], - customProp: 'custom value', - } - - mockProvider1.getNetworks.mockResolvedValue([networkWithExtraProps]) - - const { result } = renderHook(() => useBridgeSupportedNetworks(), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toHaveLength(1) - expect(result.current.data![0]).toEqual(networkWithExtraProps) - }) - }) -}) - -describe('useBridgeSupportedNetwork', () => { - beforeEach(() => { - jest.clearAllMocks() - mockUseBridgeProviders.mockReturnValue([mockProvider1, mockProvider2]) - }) - - it('should return undefined when chainId is undefined', () => { - mockProvider1.getNetworks.mockResolvedValue(chains) - - const { result } = renderHook(() => useBridgeSupportedNetwork(undefined), { wrapper }) - - expect(result.current).toBeUndefined() - }) - - it('should return the correct network when chainId is provided', async () => { - mockProvider1.getNetworks.mockResolvedValue(chains) - - const { result } = renderHook(() => useBridgeSupportedNetwork(SupportedChainId.ARBITRUM_ONE), { wrapper }) - - await waitFor(() => { - expect(result.current).toEqual(ALL_SUPPORTED_CHAINS_MAP[SupportedChainId.ARBITRUM_ONE]) - }) - }) - - it('should return undefined when chainId is not found in networks', async () => { - mockProvider1.getNetworks.mockResolvedValue(chains) - - const { result } = renderHook(() => useBridgeSupportedNetwork(999), { wrapper }) - - await waitFor(() => { - expect(result.current).toBeUndefined() - }) - }) - - it('should update when chainId changes', async () => { - mockProvider1.getNetworks.mockResolvedValue(chains) - - const { result, rerender } = renderHook(({ chainId }) => useBridgeSupportedNetwork(chainId), { - wrapper, - initialProps: { chainId: SupportedChainId.MAINNET }, - }) - - await waitFor(() => { - expect(result.current).toEqual(ALL_SUPPORTED_CHAINS_MAP[SupportedChainId.MAINNET]) - }) - - rerender({ chainId: SupportedChainId.ARBITRUM_ONE }) - - await waitFor(() => { - expect(result.current).toEqual(ALL_SUPPORTED_CHAINS_MAP[SupportedChainId.ARBITRUM_ONE]) - }) - }) - - it('should return undefined when networks data is not loaded', () => { - const { result } = renderHook(() => useBridgeSupportedNetwork(1), { wrapper }) - - expect(result.current).toBeUndefined() - }) - - it('should memoize result properly', async () => { - mockProvider1.getNetworks.mockResolvedValue(chains) - - const { result, rerender } = renderHook(({ chainId }) => useBridgeSupportedNetwork(chainId), { - wrapper, - initialProps: { chainId: 1 }, - }) - - await waitFor(() => { - expect(result.current).toEqual(chains[0]) - }) - - const firstResult = result.current - - rerender({ chainId: 1 }) // Same chainId - - expect(result.current).toBe(firstResult) // Should be the same reference due to memoization - }) -}) diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.ts index 9147427124..5f78dad164 100644 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.ts +++ b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedNetworks.ts @@ -4,36 +4,18 @@ import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' import type { ChainInfo } from '@cowprotocol/cow-sdk' import useSWR, { SWRResponse } from 'swr' +import { bridgingSdk } from 'tradingSdk/bridgingSdk' -import { useBridgeProviders } from './useBridgeProviders' +import { useBridgeProvidersIds } from './useBridgeProvidersIds' export function useBridgeSupportedNetworks(): SWRResponse { - const bridgeProviders = useBridgeProviders() - const providerIds = bridgeProviders.map((i) => i.info.dappId).join('|') + const providerIds = useBridgeProvidersIds() + const key = providerIds.join('|') return useSWR( - [providerIds, 'useBridgeSupportedNetworks'], + [key, 'useBridgeSupportedNetworks'], async () => { - const results = await Promise.allSettled( - bridgeProviders.map((provider) => { - return provider.getNetworks() - }), - ) - - const allNetworks = results.reduce>((acc, val) => { - if (val.status === 'fulfilled') { - const networks = val.value - - networks.forEach((network) => { - if (!acc[network.id]) { - acc[network.id] = network - } - }) - } - - return acc - }, {}) - return Object.values(allNetworks) + return bridgingSdk.getTargetNetworks() }, SWR_NO_REFRESH_OPTIONS, ) diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.test.tsx b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.test.tsx deleted file mode 100644 index 8570e0dbe4..0000000000 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.test.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React, { ReactNode } from 'react' - -import { TokenWithLogo } from '@cowprotocol/common-const' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { BridgeProvider, BridgeQuoteResult, BuyTokensParams } from '@cowprotocol/sdk-bridging' - -import { renderHook, waitFor } from '@testing-library/react' -import { SWRConfig } from 'swr' - -import { useBridgeProviders } from './useBridgeProviders' -import { useBridgeSupportedTokens } from './useBridgeSupportedTokens' - -jest.mock('@cowprotocol/common-hooks', () => ({ - useIsBridgingEnabled: jest.fn(), -})) - -jest.mock('./useBridgeProviders', () => ({ - useBridgeProviders: jest.fn(), -})) - -const { useIsBridgingEnabled } = require('@cowprotocol/common-hooks') - -const mockUseBridgeProviders = useBridgeProviders as jest.MockedFunction -const mockUseIsBridgingEnabled = useIsBridgingEnabled as jest.MockedFunction - -// Mock console.error to avoid noise in tests -jest.spyOn(console, 'error').mockImplementation(() => {}) - -const mockProvider1: jest.Mocked> = { - info: { dappId: 'provider1', name: 'Provider 1', logoUrl: '', website: '' }, - getBuyTokens: jest.fn(), - getNetworks: jest.fn(), - getIntermediateTokens: jest.fn(), - getQuote: jest.fn(), - getUnsignedBridgeCall: jest.fn(), - getGasLimitEstimationForHook: jest.fn(), - getSignedHook: jest.fn(), - decodeBridgeHook: jest.fn(), - getBridgingParams: jest.fn(), - getExplorerUrl: jest.fn(), - getStatus: jest.fn(), - getCancelBridgingTx: jest.fn(), - getRefundBridgingTx: jest.fn(), -} - -const mockProvider2: jest.Mocked> = { - info: { dappId: 'provider2', name: 'Provider 2', logoUrl: '', website: '' }, - getBuyTokens: jest.fn(), - getNetworks: jest.fn(), - getIntermediateTokens: jest.fn(), - getQuote: jest.fn(), - getUnsignedBridgeCall: jest.fn(), - getGasLimitEstimationForHook: jest.fn(), - getSignedHook: jest.fn(), - decodeBridgeHook: jest.fn(), - getBridgingParams: jest.fn(), - getExplorerUrl: jest.fn(), - getStatus: jest.fn(), - getCancelBridgingTx: jest.fn(), - getRefundBridgingTx: jest.fn(), -} - -const mockTokens = [ - { - address: '0x1111111111111111111111111111111111111111', - name: 'Token A', - symbol: 'TKNA', - decimals: 18, - chainId: 1, - logoUrl: 'https://example.com/tokena.png', - }, - { - address: '0x2222222222222222222222222222222222222222', - name: 'Token B', - symbol: 'TKNB', - decimals: 6, - chainId: 1, - logoUrl: 'https://example.com/tokenb.png', - }, -] - -const mockBuyTokensParams: BuyTokensParams = { - buyChainId: 1, // MAINNET - sellChainId: SupportedChainId.ARBITRUM_ONE, - sellTokenAddress: '0x1111111111111111111111111111111111111111', -} - -const wrapper = ({ children }: { children: React.ReactNode }): ReactNode => { - return new Map() }}>{children} -} - -describe('useBridgeSupportedTokens', () => { - beforeEach(() => { - jest.clearAllMocks() - mockUseIsBridgingEnabled.mockReturnValue(true) - mockUseBridgeProviders.mockReturnValue([mockProvider1, mockProvider2]) - }) - - afterEach(() => { - jest.clearAllTimers() - }) - - it('should return null when bridging is disabled', async () => { - mockUseIsBridgingEnabled.mockReturnValue(false) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - expect(result.current.data).toBeUndefined() - expect(result.current.isLoading).toBe(false) - }) - - it('should return null when params are undefined', async () => { - const { result } = renderHook(() => useBridgeSupportedTokens(undefined), { wrapper }) - - await waitFor(() => { - expect(result.current.data).toBe(null) - expect(result.current.isLoading).toBe(false) - }) - }) - - it('should fetch and combine tokens from multiple providers', async () => { - mockProvider1.getBuyTokens.mockResolvedValue({ - tokens: [mockTokens[0]], - isRouteAvailable: true, - }) - - mockProvider2.getBuyTokens.mockResolvedValue({ - tokens: [mockTokens[1]], - isRouteAvailable: true, - }) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - await waitFor(() => { - expect(result.current.data).not.toBeNull() - expect(result.current.data?.isRouteAvailable).toBe(true) - expect(result.current.data?.tokens).toHaveLength(2) - }) - - expect(mockProvider1.getBuyTokens).toHaveBeenCalledWith(mockBuyTokensParams) - expect(mockProvider2.getBuyTokens).toHaveBeenCalledWith(mockBuyTokensParams) - }) - - it('should handle duplicate tokens by keeping only one instance', async () => { - const duplicateToken = mockTokens[0] - - mockProvider1.getBuyTokens.mockResolvedValue({ - tokens: [duplicateToken], - isRouteAvailable: true, - }) - - mockProvider2.getBuyTokens.mockResolvedValue({ - tokens: [duplicateToken], - isRouteAvailable: true, - }) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - await waitFor(() => { - expect(result.current.data).not.toBeNull() - expect(result.current.data?.tokens).toHaveLength(1) - expect(result.current.data?.tokens[0]).toBeInstanceOf(TokenWithLogo) - }) - }) - - it('should set isRouteAvailable to false when no providers have routes available', async () => { - mockProvider1.getBuyTokens.mockResolvedValue({ - tokens: [], - isRouteAvailable: false, - }) - - mockProvider2.getBuyTokens.mockResolvedValue({ - tokens: [], - isRouteAvailable: false, - }) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - await waitFor(() => { - expect(result.current.data).not.toBeNull() - expect(result.current.data?.isRouteAvailable).toBe(false) - expect(result.current.data?.tokens).toHaveLength(0) - }) - }) - - it('should handle provider failures gracefully', async () => { - mockProvider1.getBuyTokens.mockRejectedValue(new Error('Provider 1 failed')) - mockProvider2.getBuyTokens.mockResolvedValue({ - tokens: [mockTokens[0]], - isRouteAvailable: true, - }) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - await waitFor(() => { - expect(result.current.data).not.toBeNull() - expect(result.current.data?.isRouteAvailable).toBe(true) - expect(result.current.data?.tokens).toHaveLength(1) - }) - }) - - it('should create proper cache key with all parameters', () => { - const { rerender } = renderHook(({ params }) => useBridgeSupportedTokens(params), { - wrapper, - initialProps: { params: mockBuyTokensParams }, - }) - - // Change params to trigger re-fetch - const newParams = { ...mockBuyTokensParams, sellChainId: SupportedChainId.GNOSIS_CHAIN } - rerender({ params: newParams }) - - expect(mockProvider1.getBuyTokens).toHaveBeenCalledWith(mockBuyTokensParams) - }) - - it('should not fetch when bridging is disabled', () => { - mockUseIsBridgingEnabled.mockReturnValue(false) - - renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - expect(mockProvider1.getBuyTokens).not.toHaveBeenCalled() - expect(mockProvider2.getBuyTokens).not.toHaveBeenCalled() - }) - - it('should handle mixed provider results correctly', async () => { - mockProvider1.getBuyTokens.mockResolvedValue({ - tokens: [mockTokens[0]], - isRouteAvailable: true, - }) - - mockProvider2.getBuyTokens.mockResolvedValue({ - tokens: [], - isRouteAvailable: false, - }) - - const { result } = renderHook(() => useBridgeSupportedTokens(mockBuyTokensParams), { wrapper }) - - await waitFor(() => { - expect(result.current.data).not.toBeNull() - expect(result.current.data?.isRouteAvailable).toBe(true) - expect(result.current.data?.tokens).toHaveLength(1) - }) - }) -}) diff --git a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.ts b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.ts index d9eab8025d..5e77312d7f 100644 --- a/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.ts +++ b/apps/cowswap-frontend/src/entities/bridgeProvider/useBridgeSupportedTokens.ts @@ -3,8 +3,9 @@ import { useIsBridgingEnabled } from '@cowprotocol/common-hooks' import { BuyTokensParams } from '@cowprotocol/sdk-bridging' import useSWR, { SWRResponse } from 'swr' +import { bridgingSdk } from 'tradingSdk/bridgingSdk' -import { useBridgeProviders } from './useBridgeProviders' +import { useBridgeProvidersIds } from './useBridgeProvidersIds' export type BridgeSupportedToken = { tokens: TokenWithLogo[]; isRouteAvailable: boolean } @@ -12,63 +13,31 @@ export function useBridgeSupportedTokens( params: BuyTokensParams | undefined, ): SWRResponse { const isBridgingEnabled = useIsBridgingEnabled() - - const bridgeProviders = useBridgeProviders() - const providerIds = bridgeProviders.map((i) => i.info.dappId).join('|') + const providerIds = useBridgeProvidersIds() + const key = providerIds.join('|') return useSWR( isBridgingEnabled - ? [ - params, - params?.sellChainId, - params?.buyChainId, - params?.sellTokenAddress, - providerIds, - 'useBridgeSupportedTokens', - ] + ? [params, params?.sellChainId, params?.buyChainId, params?.sellTokenAddress, key, 'useBridgeSupportedTokens'] : null, async ([params]) => { if (typeof params === 'undefined') return null - const results = await Promise.allSettled( - bridgeProviders.map((provider) => { - return provider.getBuyTokens(params) - }), - ) - - const state = results.reduce<{ tokens: Record; isRouteAvailable: boolean }>( - (acc, val) => { - if (val.status === 'fulfilled') { - const { tokens, isRouteAvailable } = val.value - - if (isRouteAvailable) { - tokens.forEach((token) => { - const address = token.address.toLowerCase() - - if (!acc.tokens[address]) { - acc.tokens[address] = TokenWithLogo.fromToken( - { - ...token, - name: token.name || '', - symbol: token.symbol || '', - }, - token.logoUrl, - ) - } - }) - - acc.isRouteAvailable = isBridgingEnabled ? isRouteAvailable : true - } - } - - return acc - }, - { isRouteAvailable: false, tokens: {} }, - ) - return { - isRouteAvailable: state.isRouteAvailable, - tokens: Object.values(state.tokens), - } + return bridgingSdk.getBuyTokens(params).then((result) => { + return { + isRouteAvailable: result.isRouteAvailable, + tokens: result.tokens.map((token) => + TokenWithLogo.fromToken( + { + ...token, + name: token.name || '', + symbol: token.symbol || '', + }, + token.logoUrl, + ), + ), + } + }) }, SWR_NO_REFRESH_OPTIONS, ) diff --git a/apps/cowswap-frontend/src/entities/trade/signingStepAtom.ts b/apps/cowswap-frontend/src/entities/trade/signingStepAtom.ts index 1ae9ab03db..8c8c4326d0 100644 --- a/apps/cowswap-frontend/src/entities/trade/signingStepAtom.ts +++ b/apps/cowswap-frontend/src/entities/trade/signingStepAtom.ts @@ -3,6 +3,7 @@ import { atom } from 'jotai' export enum SigningSteps { PermitSigning = 'PermitSigning', BridgingSigning = 'BridgingSigning', + PreparingDepositAddress = 'PreparingDepositAddress', OrderSigning = 'OrderSigning', } diff --git a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts index 9cf117e154..902a9c14da 100644 --- a/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts +++ b/apps/cowswap-frontend/src/legacy/hooks/usePriceImpact/useFiatValuePriceImpact.ts @@ -2,10 +2,9 @@ import { useMemo } from 'react' import { ONE_HUNDRED_PERCENT } from '@cowprotocol/common-const' import { useDebounce } from '@cowprotocol/common-hooks' -import { getWrappedToken } from '@cowprotocol/common-utils' -import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils' +import { Fraction, Percent } from '@uniswap/sdk-core' -import JSBI from 'jsbi' import ms from 'ms.macro' import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' @@ -37,18 +36,22 @@ export function useFiatValuePriceImpact() { // Don't calculate price impact if trade is not set up (both trade assets are not set) if (!isTradeSetUp) return null - const priceImpact = computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput) + const priceImpact = computeFiatValuePriceImpact( + fiatValueInput ? FractionUtils.fractionLikeToFraction(fiatValueInput) : null, + fiatValueOutput ? FractionUtils.fractionLikeToFraction(fiatValueOutput) : null, + ) return { priceImpact, isLoading } }, [isTradeSetUp, fiatValueInput, fiatValueOutput, isLoading]) } function computeFiatValuePriceImpact( - fiatValueInput: CurrencyAmount | undefined | null, - fiatValueOutput: CurrencyAmount | undefined | null, + fiatValueInput: Fraction | null, + fiatValueOutput: Fraction | null, ): Percent | undefined { if (!fiatValueOutput || !fiatValueInput) return undefined - if (JSBI.equal(fiatValueInput.quotient, JSBI.BigInt(0))) return undefined + const fiatValueInputNum = +fiatValueInput.toFixed(6) + if (!fiatValueInputNum || fiatValueInputNum <= 0) return undefined const pct = ONE_HUNDRED_PERCENT.subtract(fiatValueOutput.divide(fiatValueInput)) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index 85dd692ccd..66a09eacdc 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -1942,6 +1942,7 @@ msgstr "(via WalletConnect)" msgid "<0>Select an {accountProxyLabelString} and then select a token you want to recover from CoW Shed." msgstr "<0>Select an {accountProxyLabelString} and then select a token you want to recover from CoW Shed." +#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx msgid "Error loading price. Try again later." msgstr "Error loading price. Try again later." @@ -2147,6 +2148,10 @@ msgstr "View on Safe" msgid "modified" msgstr "modified" +#: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/getPendingText.ts +msgid "Preparing deposit" +msgstr "Preparing deposit" + #: apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx #: apps/cowswap-frontend/src/modules/bridge/pure/ProtocolIcons/StackedProtocolIcons.tsx msgid "CoW Protocol" @@ -2942,6 +2947,10 @@ msgstr "The time each part of your TWAP order will remain active." msgid "Buy per part" msgstr "Buy per part" +#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx +msgid "No intermediate tokens found for the route" +msgstr "No intermediate tokens found for the route" + #: apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx msgid "Total claimable rewards:" msgstr "Total claimable rewards:" @@ -4659,6 +4668,10 @@ msgstr "Boost Your Yield with One-Click Conversion" msgid "Vested" msgstr "Vested" +#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx +msgid "Bridging deposit address is not verified! Please contact CoW Swap support!" +msgstr "Bridging deposit address is not verified! Please contact CoW Swap support!" + #: apps/cowswap-frontend/src/modules/wallet/pure/WalletModal/styled.tsx msgid "Learn more about wallets" msgstr "Learn more about wallets" diff --git a/apps/cowswap-frontend/src/modules/accountProxy/containers/ProxyRecipient/index.tsx b/apps/cowswap-frontend/src/modules/accountProxy/containers/ProxyRecipient/index.tsx index d1abd50e1a..7c374d96ab 100644 --- a/apps/cowswap-frontend/src/modules/accountProxy/containers/ProxyRecipient/index.tsx +++ b/apps/cowswap-frontend/src/modules/accountProxy/containers/ProxyRecipient/index.tsx @@ -1,7 +1,8 @@ import { ReactNode } from 'react' import { ACCOUNT_PROXY_LABEL } from '@cowprotocol/common-const' -import { areAddressesEqual, isProdLike } from '@cowprotocol/common-utils' +import { areAddressesEqual } from '@cowprotocol/common-utils' +import { InfoTooltip } from '@cowprotocol/ui' import { t } from '@lingui/core/macro' import { useLingui } from '@lingui/react/macro' @@ -36,14 +37,15 @@ export function ProxyRecipient({ chainId, size = 14, }: ProxyRecipientProps): ReactNode { - !isProdLike && console.debug('[ProxyRecipient] recipient', { recipient, bridgeReceiverOverride }) const proxyAddress = useCurrentAccountProxyAddress() const { i18n } = useLingui() const accountProxyLabelString = i18n._(ACCOUNT_PROXY_LABEL) - if (!recipient || !(proxyAddress && !bridgeReceiverOverride)) return null + const targetAddress = bridgeReceiverOverride || proxyAddress - if (!bridgeReceiverOverride && !areAddressesEqual(recipient, proxyAddress)) { + if (!targetAddress) return + + if (!bridgeReceiverOverride && proxyAddress && recipient && !areAddressesEqual(recipient, proxyAddress)) { throw new Error( t`Provided proxy address does not match ${accountProxyLabelString} address!, recipient=${recipient}, proxyAddress=${proxyAddress}`, ) @@ -51,8 +53,16 @@ export function ProxyRecipient({ return ( - - + {bridgeReceiverOverride ? ( + + ) : ( + + )} + ) } diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts index e379da1965..21bb1712bd 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react' +import { getCurrencyAddress } from '@cowprotocol/common-utils' import { cowAppDataLatestScheme } from '@cowprotocol/cow-sdk' import { PermitHookData } from '@cowprotocol/permit-utils' import { useIsSmartContractWallet } from '@cowprotocol/wallet' @@ -50,6 +51,17 @@ export function AppDataHooksUpdater(): null { const [permitHook, setPermitHook] = useState(undefined) + const inputCurrencyAddress = tradeState?.inputCurrency ? getCurrencyAddress(tradeState.inputCurrency) : undefined + + /** + * Reset appDataHooks every time sellToken changes + */ + useEffect(() => { + if (!inputCurrencyAddress) return + + updateAppDataHooks(undefined) + }, [inputCurrencyAddress, updateAppDataHooks]) + useEffect(() => { const preInteractionHooks = (preHooks || []).map((hookDetails) => cowHookToTypedCowHook(hookDetails.hook, 'hookStore'), diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index d6bc842725..85e880295f 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -8,7 +8,7 @@ import { HwAccountIndexUpdater, useWalletInfo, WalletUpdater } from '@cowprotoco import { CowSdkUpdater } from 'cowSdk' import { useBalancesContext } from 'entities/balancesContext/useBalancesContext' import { BridgeOrdersCleanUpdater } from 'entities/bridgeOrders' -import { useBridgeSupportedNetworks } from 'entities/bridgeProvider' +import { BridgeProvidersUpdater, useBridgeSupportedNetworks } from 'entities/bridgeProvider' import { ThemeConfigUpdater } from 'theme/ThemeConfigUpdater' import { TradingSdkUpdater } from 'tradingSdk/TradingSdkUpdater' @@ -68,6 +68,8 @@ export function Updaters(): ReactNode { return ( <> + + @@ -75,7 +77,6 @@ export function Updaters(): ReactNode { {/*Set custom chainId only when it differs from the wallet chainId*/} {/*MultiCallUpdater will use wallet network by default if custom chainId is not provided*/} - diff --git a/apps/cowswap-frontend/src/modules/bridge/hooks/useBridgeQuoteAmounts.ts b/apps/cowswap-frontend/src/modules/bridge/hooks/useBridgeQuoteAmounts.ts index 0608787939..fb26fad16b 100644 --- a/apps/cowswap-frontend/src/modules/bridge/hooks/useBridgeQuoteAmounts.ts +++ b/apps/cowswap-frontend/src/modules/bridge/hooks/useBridgeQuoteAmounts.ts @@ -8,8 +8,8 @@ import { useTradeQuote } from 'modules/tradeQuote' import { useTryFindIntermediateToken } from './useTryFindIntermediateToken' -export function useBridgeQuoteAmounts(): BridgeQuoteAmounts | null { - const receiveAmountInfo = useGetReceiveAmountInfo() +export function useBridgeQuoteAmounts(overrideBridgeBuyAmount = true): BridgeQuoteAmounts | null { + const receiveAmountInfo = useGetReceiveAmountInfo(overrideBridgeBuyAmount) const { bridgeQuote } = useTradeQuote() const { intermediateBuyToken } = useTryFindIntermediateToken({ bridgeQuote }) @@ -42,6 +42,7 @@ export function useBridgeQuoteAmounts(): BridgeQuoteAmounts | null { swapMinReceiveAmount, bridgeMinReceiveAmount, bridgeFee: receiveAmountInfo.costs.bridgeFee.amountInDestinationCurrency, + bridgeFeeAmounts: receiveAmountInfo.costs.bridgeFee, } }, [receiveAmountInfo, bridgeQuote, intermediateBuyToken]) } diff --git a/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteBridgeContext.ts b/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteBridgeContext.ts index 20ec39282c..04edb7b09a 100644 --- a/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteBridgeContext.ts +++ b/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteBridgeContext.ts @@ -17,20 +17,16 @@ import { QuoteBridgeContext } from '../types' export function useQuoteBridgeContext(): QuoteBridgeContext | null { const { bridgeQuote } = useTradeQuote() - const quoteAmounts = useBridgeQuoteAmounts() + const quoteAmounts = useBridgeQuoteAmounts(true) - /** - * Convert buy amount from intermediate currency to destination currency - * After that we substract bridging costs - */ const buyAmount = useMemo(() => { - if (!quoteAmounts?.bridgeFee) return + if (!bridgeQuote || !quoteAmounts) return null return CurrencyAmount.fromRawAmount( - quoteAmounts.bridgeFee.currency, - quoteAmounts.swapBuyAmount.quotient.toString(), - ).subtract(quoteAmounts.bridgeFee) - }, [quoteAmounts]) + quoteAmounts.bridgeMinReceiveAmount.currency, + bridgeQuote?.amountsAndCosts.beforeFee.buyAmount.toString(), + ) + }, [quoteAmounts, bridgeQuote]) const { value: buyAmountUsd } = useUsdAmount(buyAmount) const { value: bridgeMinDepositAmountUsd } = useUsdAmount(quoteAmounts?.swapMinReceiveAmount) diff --git a/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteSwapContext.ts b/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteSwapContext.ts index 94a2de092f..5bd7546f7e 100644 --- a/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteSwapContext.ts +++ b/apps/cowswap-frontend/src/modules/bridge/hooks/useQuoteSwapContext.ts @@ -14,7 +14,7 @@ import { useBridgeQuoteAmounts } from './useBridgeQuoteAmounts' import { QuoteSwapContext } from '../types' export function useQuoteSwapContext(): QuoteSwapContext | null { - const receiveAmountInfo = useGetReceiveAmountInfo() + const receiveAmountInfo = useGetReceiveAmountInfo(true) const quoteAmounts = useBridgeQuoteAmounts() const { value: swapMinReceiveAmountUsd } = useUsdAmount(quoteAmounts?.swapMinReceiveAmount) diff --git a/apps/cowswap-frontend/src/modules/ethFlow/services/ethFlow/index.ts b/apps/cowswap-frontend/src/modules/ethFlow/services/ethFlow/index.ts index cc2d6aa48c..66fd5a7702 100644 --- a/apps/cowswap-frontend/src/modules/ethFlow/services/ethFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/ethFlow/services/ethFlow/index.ts @@ -95,7 +95,13 @@ export async function ethFlow({ const signingStepManager: SigningStepManager = { beforeBridgingSign() { - setSigningStep('1/2', SigningSteps.BridgingSigning) + const isReceiverAccountBridgeProvider = + tradeQuoteState.bridgeQuote?.providerInfo.type === 'ReceiverAccountBridgeProvider' + + setSigningStep( + '1/2', + isReceiverAccountBridgeProvider ? SigningSteps.PreparingDepositAddress : SigningSteps.BridgingSigning, + ) }, beforeOrderSign() { setSigningStep('2/2', SigningSteps.OrderSigning) diff --git a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx index 2ff47fab00..46f9ae94b2 100644 --- a/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/orderProgressBar/pure/OrderProgressBar/index.cosmos.tsx @@ -42,6 +42,7 @@ const swapAndBridgeContextMock: SwapAndBridgeContext = { name: 'Across', dappId: 'cow-sdk://bridging/providers/across', website: 'https://across.to', + type: 'HookBridgeProvider', }, overview: { sourceChainName: 'Ethereum', diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index 7ef3d22c60..8a173c4b4c 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -23,6 +23,7 @@ import { TradeType } from 'modules/trade' import { useTradeTypeInfoFromUrl } from 'modules/trade/hooks/useTradeTypeInfoFromUrl' import { useIsAlternativeOrderModalVisible } from 'modules/trade/state/alternativeOrder' import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' +import { useTradeQuote } from 'modules/tradeQuote' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useThrottleFn } from 'common/hooks/useThrottleFn' @@ -105,6 +106,9 @@ export function TradeWidgetForm(props: TradeWidgetProps): ReactNode { [isWrapOrUnwrap, props.outputCurrencyInfo, props.inputCurrencyInfo.amount], ) + const { bridgeQuote } = useTradeQuote() + const isReceiverAccountBridgeProvider = bridgeQuote?.providerInfo.type === 'ReceiverAccountBridgeProvider' + const { chainId, account } = useWalletInfo() const { allowsOffchainSigning } = useWalletDetails() const isChainIdUnsupported = useIsProviderNetworkUnsupported() @@ -249,6 +253,7 @@ export function TradeWidgetForm(props: TradeWidgetProps): ReactNode {
= { [SigningSteps.PermitSigning]: msg`Confirm approval`, [SigningSteps.BridgingSigning]: msg`Confirm bridging`, + [SigningSteps.PreparingDepositAddress]: msg`Preparing deposit`, [SigningSteps.OrderSigning]: msg`Confirm swap`, } diff --git a/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts deleted file mode 100644 index 339e55a955..0000000000 --- a/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { atom } from 'jotai' - -import { getCurrencyAddress, isFractionFalsy } from '@cowprotocol/common-utils' - -import { tradeQuotesAtom } from 'modules/tradeQuote' -import { volumeFeeAtom } from 'modules/volumeFee' - -import { derivedTradeStateAtom } from './derivedTradeStateAtom' - -import { getReceiveAmountInfo } from '../utils/getReceiveAmountInfo' - -// eslint-disable-next-line complexity -export const receiveAmountInfoAtom = atom((get) => { - const tradeQuotes = get(tradeQuotesAtom) - const volumeFee = get(volumeFeeAtom) - const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, slippage, orderKind } = - get(derivedTradeStateAtom) || {} - const quoteResponse = - inputCurrency && tradeQuotes[getCurrencyAddress(inputCurrency).toLowerCase()]?.quote?.quoteResults.quoteResponse - - if (isFractionFalsy(inputCurrencyAmount) && isFractionFalsy(outputCurrencyAmount)) return null - - // Avoid states mismatch - if (orderKind !== quoteResponse?.quote.kind) return null - - if (quoteResponse && inputCurrency && outputCurrency && slippage) { - return getReceiveAmountInfo(quoteResponse.quote, inputCurrency, outputCurrency, slippage, volumeFee?.volumeBps) - } - - return null -}) diff --git a/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.test.ts b/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.test.ts new file mode 100644 index 0000000000..45d91de9e1 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.test.ts @@ -0,0 +1,190 @@ +import { OrderKind } from '@cowprotocol/cow-sdk' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Percent, Token } from '@uniswap/sdk-core' + +import { getReceiveAmountInfo } from './getReceiveAmountInfo' + +const toAddress = (suffix: string): string => `0x${suffix.padStart(40, '0')}` + +const mainnetUsdc = new Token(SupportedChainId.MAINNET, toAddress('1'), 6, 'USDC', 'USD Coin') +const mainnetWeth = new Token(SupportedChainId.MAINNET, toAddress('2'), 18, 'WETH', 'Wrapped Ether') +const baseUsdc = new Token(SupportedChainId.BASE, toAddress('3'), 6, 'USDC', 'USD Coin') +const baseWeth = new Token(SupportedChainId.BASE, toAddress('4'), 18, 'WETH', 'Wrapped Ether') + +describe('getReceiveAmountInfo', () => { + describe('bridge fee calculation with different decimals', () => { + it('calculates bridge fee correctly when intermediate and destination have same decimals', () => { + const orderParams = { + kind: OrderKind.SELL, + sellToken: mainnetUsdc.address, + buyToken: baseUsdc.address, + sellAmount: '1000000', // 1 USDC (6 decimals) + buyAmount: '950000', // 0.95 USDC (6 decimals) + validTo: Math.floor(Date.now() / 1000) + 3600, + appData: '0x0000000000000000000000000000000000000000000000000000000000000000', + feeAmount: '10000', // 0.01 USDC + partiallyFillable: false, + } + + const slippagePercent = new Percent(50, 10000) // 0.5% + const bridgeFeeAmounts = { + amountInSellCurrency: BigInt('5000'), // 0.005 USDC in destination decimals (6) + amountInBuyCurrency: BigInt('5000'), // 0.005 USDC in intermediate decimals (6) + } + + const result = getReceiveAmountInfo( + orderParams, + mainnetUsdc, + baseUsdc, + slippagePercent, + undefined, + mainnetUsdc, // intermediate currency + bridgeFeeAmounts, + ) + + expect(result.costs.bridgeFee).toBeDefined() + expect(result.costs.bridgeFee?.amountInDestinationCurrency.toExact()).toBe('0.005') + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.toExact()).toBe('0.005') + expect(result.costs.bridgeFee?.amountInDestinationCurrency.currency.decimals).toBe(6) + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.currency.decimals).toBe(6) + }) + + it('adjusts bridge fee decimals when intermediate has 18 decimals and destination has 6', () => { + const orderParams = { + kind: OrderKind.SELL, + sellToken: mainnetWeth.address, + buyToken: baseUsdc.address, + sellAmount: '1000000000000000000', // 1 WETH (18 decimals) + buyAmount: '2000000', // 2 USDC (6 decimals) + validTo: Math.floor(Date.now() / 1000) + 3600, + appData: '0x0000000000000000000000000000000000000000000000000000000000000000', + feeAmount: '10000000000000000', // 0.01 WETH + partiallyFillable: false, + } + + const slippagePercent = new Percent(50, 10000) // 0.5% + const bridgeFeeAmounts = { + amountInSellCurrency: BigInt('50000'), // 0.05 USDC in destination decimals (6) + amountInBuyCurrency: BigInt('50000000000000000'), // 0.05 in intermediate decimals (18) + } + + const result = getReceiveAmountInfo( + orderParams, + mainnetWeth, + baseUsdc, + slippagePercent, + undefined, + mainnetWeth, // intermediate currency with 18 decimals + bridgeFeeAmounts, + ) + + expect(result.costs.bridgeFee).toBeDefined() + // Destination currency amount should be 0.05 USDC (6 decimals) + expect(result.costs.bridgeFee?.amountInDestinationCurrency.toExact()).toBe('0.05') + expect(result.costs.bridgeFee?.amountInDestinationCurrency.currency.decimals).toBe(6) + + // Intermediate currency amount is in WETH (18 decimals) + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.toExact()).toBe('0.05') + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.currency.decimals).toBe(18) + }) + + it('adjusts bridge fee decimals when intermediate has 6 decimals and destination has 18', () => { + const orderParams = { + kind: OrderKind.SELL, + sellToken: mainnetUsdc.address, + buyToken: baseWeth.address, + sellAmount: '2000000', // 2 USDC (6 decimals) + buyAmount: '1000000000000000000', // 1 WETH (18 decimals) + validTo: Math.floor(Date.now() / 1000) + 3600, + appData: '0x0000000000000000000000000000000000000000000000000000000000000000', + feeAmount: '10000', // 0.01 USDC + partiallyFillable: false, + } + + const slippagePercent = new Percent(50, 10000) // 0.5% + const bridgeFeeAmounts = { + amountInSellCurrency: BigInt('50000000000000000'), // 0.05 WETH in destination decimals (18) + amountInBuyCurrency: BigInt('50000'), // 0.05 in intermediate decimals (6) + } + + const result = getReceiveAmountInfo( + orderParams, + mainnetUsdc, + baseWeth, + slippagePercent, + undefined, + mainnetUsdc, // intermediate currency with 6 decimals + bridgeFeeAmounts, + ) + + expect(result.costs.bridgeFee).toBeDefined() + // Destination currency amount should be 0.05 WETH (18 decimals) + expect(result.costs.bridgeFee?.amountInDestinationCurrency.toExact()).toBe('0.05') + expect(result.costs.bridgeFee?.amountInDestinationCurrency.currency.decimals).toBe(18) + + // Intermediate currency amount is in USDC (6 decimals) + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.toExact()).toBe('0.05') + expect(result.costs.bridgeFee?.amountInIntermediateCurrency.currency.decimals).toBe(6) + }) + + it('returns undefined bridge fee when bridgeFeeAmounts is not provided', () => { + const orderParams = { + kind: OrderKind.SELL, + sellToken: mainnetUsdc.address, + buyToken: baseUsdc.address, + sellAmount: '1000000', + buyAmount: '950000', + validTo: Math.floor(Date.now() / 1000) + 3600, + appData: '0x0000000000000000000000000000000000000000000000000000000000000000', + feeAmount: '10000', + partiallyFillable: false, + } + + const slippagePercent = new Percent(50, 10000) // 0.5% + + const result = getReceiveAmountInfo( + orderParams, + mainnetUsdc, + baseUsdc, + slippagePercent, + undefined, + mainnetUsdc, + undefined, // no bridge fee amounts + ) + + expect(result.costs.bridgeFee).toBeUndefined() + }) + + it('returns undefined bridge fee when intermediate currency is not provided', () => { + const orderParams = { + kind: OrderKind.SELL, + sellToken: mainnetUsdc.address, + buyToken: baseUsdc.address, + sellAmount: '1000000', + buyAmount: '950000', + validTo: Math.floor(Date.now() / 1000) + 3600, + appData: '0x0000000000000000000000000000000000000000000000000000000000000000', + feeAmount: '10000', + partiallyFillable: false, + } + + const slippagePercent = new Percent(50, 10000) // 0.5% + const bridgeFeeAmounts = { + amountInSellCurrency: BigInt('5000'), + amountInBuyCurrency: BigInt('5000'), + } + + const result = getReceiveAmountInfo( + orderParams, + mainnetUsdc, + baseUsdc, + slippagePercent, + undefined, + undefined, // no intermediate currency + bridgeFeeAmounts, + ) + + expect(result.costs.bridgeFee).toBeUndefined() + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.ts b/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.ts index da0a5f6d95..934cdb0c2c 100644 --- a/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.ts +++ b/apps/cowswap-frontend/src/modules/trade/utils/getReceiveAmountInfo.ts @@ -1,4 +1,4 @@ -import { isSellOrder } from '@cowprotocol/common-utils' +import { FractionUtils, isSellOrder } from '@cowprotocol/common-utils' import { type OrderParameters, getQuoteAmountsAndCosts } from '@cowprotocol/cow-sdk' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' @@ -33,7 +33,26 @@ export function getTotalCosts( const fee = networkFeeAmount.add(info.costs.partnerFee.amount) - return additionalCosts ? fee.add(additionalCosts) : fee + if (additionalCosts) { + if (!additionalCosts.currency.equals(fee.currency)) { + const additionalCostsFixed = CurrencyAmount.fromRawAmount( + fee.currency, + additionalCosts.currency.decimals !== fee.currency.decimals + ? FractionUtils.adjustDecimalsAtoms( + additionalCosts, + fee.currency.decimals, + additionalCosts.currency.decimals, + ).quotient.toString() + : additionalCosts.quotient.toString(), + ) + + return fee.add(additionalCostsFixed) + } + + return fee.add(additionalCosts) + } + + return fee } /** @@ -46,17 +65,33 @@ export function getReceiveAmountInfo( slippagePercent: Percent, _partnerFeeBps: number | undefined, intermediateCurrency?: Currency, - bridgeFeeRaw?: bigint, + bridgeFeeAmounts?: { + amountInSellCurrency: bigint + amountInBuyCurrency: bigint + }, + bridgeBuyAmount?: bigint, ): ReceiveAmountInfo { const partnerFeeBps = _partnerFeeBps ?? 0 const currenciesExcludingIntermediate = { inputCurrency, outputCurrency } const isSell = isSellOrder(orderParams.kind) + const buyToken = intermediateCurrency && bridgeBuyAmount ? intermediateCurrency : outputCurrency + const result = getQuoteAmountsAndCosts({ - orderParams, + orderParams: + intermediateCurrency && bridgeBuyAmount + ? { + ...orderParams, + buyAmount: FractionUtils.adjustDecimalsAtoms( + CurrencyAmount.fromRawAmount(intermediateCurrency, bridgeBuyAmount.toString()), + outputCurrency.decimals, + intermediateCurrency.decimals, + ).quotient.toString(), + } + : orderParams, sellDecimals: inputCurrency.decimals, - buyDecimals: outputCurrency.decimals, + buyDecimals: buyToken.decimals, slippagePercentBps: Number(slippagePercent.numerator), partnerFeeBps, }) @@ -68,13 +103,7 @@ export function getReceiveAmountInfo( const beforeNetworkCosts = mapBigIntAmounts(result.beforeNetworkCosts, currenciesWithIntermediate) const afterNetworkCosts = mapBigIntAmounts(result.afterNetworkCosts, currenciesWithIntermediate) - const bridgeFee = - typeof bridgeFeeRaw === 'bigint' && intermediateCurrency - ? { - amountInIntermediateCurrency: CurrencyAmount.fromRawAmount(intermediateCurrency, bridgeFeeRaw.toString()), - amountInDestinationCurrency: CurrencyAmount.fromRawAmount(outputCurrency, bridgeFeeRaw.toString()), - } - : undefined + const bridgeFee = calculateBridgeFee(outputCurrency, intermediateCurrency, bridgeFeeAmounts) return { ...result, @@ -109,6 +138,28 @@ export function getReceiveAmountInfo( } } +function calculateBridgeFee( + outputCurrency: Currency, + intermediateCurrency?: Currency, + bridgeFeeAmounts?: { + amountInSellCurrency: bigint + amountInBuyCurrency: bigint + }, +): ReceiveAmountInfo['costs']['bridgeFee'] | undefined { + if (!bridgeFeeAmounts || !intermediateCurrency) return undefined + + return { + amountInIntermediateCurrency: CurrencyAmount.fromRawAmount( + intermediateCurrency, + bridgeFeeAmounts.amountInBuyCurrency.toString(), + ), + amountInDestinationCurrency: CurrencyAmount.fromRawAmount( + outputCurrency, + bridgeFeeAmounts.amountInSellCurrency.toString(), + ), + } +} + function mapBigIntAmounts( amounts: { sellAmount: bigint; buyAmount: bigint }, currencies: { inputCurrency: Currency; outputCurrency: Currency }, diff --git a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts index 9f59af2751..40598edc8c 100644 --- a/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeFlow/services/swapFlow/index.ts @@ -41,6 +41,7 @@ export async function swapFlow( tradeConfirmActions, callbacks: { getCachedPermit, addBridgeOrder, setSigningStep }, tradeQuote, + tradeQuoteState, bridgeQuoteAmounts, } = input @@ -105,7 +106,13 @@ export async function swapFlow( const signingStepManager: SigningStepManager = { beforeBridgingSign() { - setSigningStep(shouldSignPermit ? '2/3' : '1/2', SigningSteps.BridgingSigning) + const isReceiverAccountBridgeProvider = + tradeQuoteState.bridgeQuote?.providerInfo.type === 'ReceiverAccountBridgeProvider' + + setSigningStep( + shouldSignPermit ? '2/3' : '1/2', + isReceiverAccountBridgeProvider ? SigningSteps.PreparingDepositAddress : SigningSteps.BridgingSigning, + ) }, afterBridgingSign() { bridgingSignTimestamp = Date.now() diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx index 5d155bd462..66e2c79419 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx @@ -3,7 +3,7 @@ import { ReactElement, ReactNode } from 'react' import { ACCOUNT_PROXY_LABEL } from '@cowprotocol/common-const' import { getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { BridgeProviderQuoteError, BridgeQuoteErrors } from '@cowprotocol/sdk-bridging' -import { CenteredDots, HelpTooltip, TokenSymbol } from '@cowprotocol/ui' +import { CenteredDots, HelpTooltip, InfoTooltip, TokenSymbol } from '@cowprotocol/ui' import { i18n } from '@lingui/core' import { t } from '@lingui/core/macro' @@ -28,6 +28,38 @@ interface ButtonCallback { (context: TradeFormButtonContext, isDisabled?: boolean): ReactElement | null } +function getDefaultQuoteError(): string { + return t`Error loading price. Try again later.` +} + +function getQuoteErrorTexts(): Record { + return { + [QuoteApiErrorCodes.UNHANDLED_ERROR]: getDefaultQuoteError(), + [QuoteApiErrorCodes.TransferEthToContract]: t`Buying native currency with smart contract wallets is not currently supported`, + [QuoteApiErrorCodes.UnsupportedToken]: t`Unsupported token`, + [QuoteApiErrorCodes.InsufficientLiquidity]: t`Insufficient liquidity for this trade.`, + [QuoteApiErrorCodes.FeeExceedsFrom]: t`Sell amount is too small`, + [QuoteApiErrorCodes.ZeroPrice]: t`Invalid price. Try increasing input/output amount.`, + [QuoteApiErrorCodes.SameBuyAndSellToken]: t`Tokens must be different`, + } +} + +function getBridgeQuoteErrorTexts(): Record { + const DEFAULT_QUOTE_ERROR = getDefaultQuoteError() + + return { + [BridgeQuoteErrors.API_ERROR]: DEFAULT_QUOTE_ERROR, + [BridgeQuoteErrors.INVALID_BRIDGE]: DEFAULT_QUOTE_ERROR, + [BridgeQuoteErrors.TX_BUILD_ERROR]: DEFAULT_QUOTE_ERROR, + [BridgeQuoteErrors.QUOTE_ERROR]: DEFAULT_QUOTE_ERROR, + [BridgeQuoteErrors.INVALID_API_JSON_RESPONSE]: DEFAULT_QUOTE_ERROR, + [BridgeQuoteErrors.NO_INTERMEDIATE_TOKENS]: t`No routes found`, + [BridgeQuoteErrors.NO_ROUTES]: t`No routes found`, + [BridgeQuoteErrors.ONLY_SELL_ORDER_SUPPORTED]: t`Only "sell" orders are supported`, + [BridgeQuoteErrors.QUOTE_DOES_NOT_MATCH_DEPOSIT_ADDRESS]: t`Bridging deposit address is not verified! Please contact CoW Swap support!`, + } +} + const CompatibilityIssuesWarningWrapper = styled.div` margin-top: -10px; ` @@ -120,30 +152,13 @@ export const tradeButtonsMap: Record { const DEFAULT_QUOTE_ERROR = t`Error loading price. Try again later.` - const quoteErrorTexts: Record = { - [QuoteApiErrorCodes.UNHANDLED_ERROR]: DEFAULT_QUOTE_ERROR, - [QuoteApiErrorCodes.TransferEthToContract]: t`Buying native currency with smart contract wallets is not currently supported`, - [QuoteApiErrorCodes.UnsupportedToken]: t`Unsupported token`, - [QuoteApiErrorCodes.InsufficientLiquidity]: t`Insufficient liquidity for this trade.`, - [QuoteApiErrorCodes.FeeExceedsFrom]: t`Sell amount is too small`, - [QuoteApiErrorCodes.ZeroPrice]: t`Invalid price. Try increasing input/output amount.`, - [QuoteApiErrorCodes.SameBuyAndSellToken]: t`Tokens must be different`, - } + const quoteErrorTexts = getQuoteErrorTexts() const quoteErrorTextsForBridges: Partial> = { [QuoteApiErrorCodes.SameBuyAndSellToken]: t`Not yet supported`, } - const bridgeQuoteErrorTexts: Record = { - [BridgeQuoteErrors.API_ERROR]: DEFAULT_QUOTE_ERROR, - [BridgeQuoteErrors.INVALID_BRIDGE]: DEFAULT_QUOTE_ERROR, - [BridgeQuoteErrors.TX_BUILD_ERROR]: DEFAULT_QUOTE_ERROR, - [BridgeQuoteErrors.QUOTE_ERROR]: DEFAULT_QUOTE_ERROR, - [BridgeQuoteErrors.INVALID_API_JSON_RESPONSE]: DEFAULT_QUOTE_ERROR, - [BridgeQuoteErrors.NO_INTERMEDIATE_TOKENS]: t`No routes found`, - [BridgeQuoteErrors.NO_ROUTES]: t`No routes found`, - [BridgeQuoteErrors.ONLY_SELL_ORDER_SUPPORTED]: t`Only "sell" orders are supported`, - } + const bridgeQuoteErrorTexts = getBridgeQuoteErrorTexts() const errorTooltipContentForBridges: Partial> = { [QuoteApiErrorCodes.SameBuyAndSellToken]: t`Bridging without swapping is not yet supported. Let us know if you want this feature!`, @@ -159,8 +174,28 @@ export const tradeButtonsMap: Record { + const quoteErrorText = quoteErrorTexts[errorType] + const bridgeQuoteErrorText = quoteErrorTextsForBridges[errorType] + + if (isBridge && bridgeQuoteErrorText) { + // Do not display "Not yet supported" when sell and intermediate tokens are the same + // Because user doesn't see intermediate token + if (errorType === QuoteApiErrorCodes.SameBuyAndSellToken) { + const areSwapAssetsDifferent = + context.derivedState.inputCurrency?.symbol?.toLowerCase() !== + context.derivedState.outputCurrency?.symbol?.toLowerCase() + + if (areSwapAssetsDifferent) { + return bridgeQuoteErrorTexts[BridgeQuoteErrors.NO_ROUTES] + } + } + + return bridgeQuoteErrorText + } + + return quoteErrorText || DEFAULT_QUOTE_ERROR + })() const errorTooltipText = isBridge && errorTooltipContentForBridges[errorType] @@ -180,7 +215,12 @@ export const tradeButtonsMap: Record - <>{errorText} + <> + {errorText} + {errorMessage === BridgeQuoteErrors.NO_INTERMEDIATE_TOKENS && ( + + )} + ) } @@ -271,9 +311,7 @@ export const tradeButtonsMap: Record - - {defaultText} - + {defaultText} ) }, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/usePollQuoteCallback.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/usePollQuoteCallback.ts index bf8ba93379..6c7b1d61d4 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/usePollQuoteCallback.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/usePollQuoteCallback.ts @@ -35,6 +35,8 @@ export function usePollQuoteCallback( // eslint-disable-next-line react-hooks/refs isOnlineRef.current = isOnline + const updatingStartTimestamp = useRef(null) + return useCallback( (hasParamsChanged: boolean, forceUpdate = false): boolean => { const { isQuoteUpdatePossible, isConfirmOpen } = quotePollingParams @@ -48,8 +50,15 @@ export function usePollQuoteCallback( return false } - const fetchQuote = (fetchParams: TradeQuoteFetchParams): Promise => - fetchAndProcessQuote(fetchParams, quoteParams, quotePollingParams, appData, tradeQuoteManager) + const fetchQuote = (fetchParams: TradeQuoteFetchParams): Promise => { + const now = Date.now() + updatingStartTimestamp.current = now + + return fetchAndProcessQuote(fetchParams, quoteParams, quotePollingParams, appData, tradeQuoteManager, { + now, + ref: updatingStartTimestamp, + }) + } const context: QuoteUpdateContext = { currentQuote: tradeQuoteRef.current, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts index 4d6f9d71a1..f4df76a6d5 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts @@ -1,6 +1,6 @@ import { DEFAULT_APP_CODE } from '@cowprotocol/common-const' import { useDebounce } from '@cowprotocol/common-hooks' -import { getCurrencyAddress, isAddress } from '@cowprotocol/common-utils' +import { getCurrencyAddress } from '@cowprotocol/common-utils' import { QuoteBridgeRequest } from '@cowprotocol/sdk-bridging' import { useWalletInfo } from '@cowprotocol/wallet' import { useWalletProvider } from '@cowprotocol/wallet-provider' @@ -18,6 +18,8 @@ import { useVolumeFee } from 'modules/volumeFee' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useSafeMemo } from 'common/hooks/useSafeMemo' +import { useQuoteParamsRecipient } from './useQuoteParamsRecipient' + import { BRIDGE_QUOTE_ACCOUNT, getBridgeQuoteSigner } from '../utils/getBridgeQuoteSigner' const DEFAULT_QUOTE_TTL = ms`30m` / 1000 @@ -41,9 +43,9 @@ export function useQuoteParams(amount: Nullish, partiallyFillable = fals const tradeSlippage = useTradeSlippageValueAndType() const slippageBps = tradeSlippage.type === 'user' ? tradeSlippage.value : undefined - const { inputCurrency, outputCurrency, orderKind, recipientAddress } = state || {} + const { inputCurrency, outputCurrency, orderKind } = state || {} - const receiver = recipientAddress && isAddress(recipientAddress) ? recipientAddress : account + const receiver = useQuoteParamsRecipient() // eslint-disable-next-line complexity const params = useSafeMemo(() => { diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.test.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.test.ts new file mode 100644 index 0000000000..f8cee71847 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.test.ts @@ -0,0 +1,182 @@ +import { useWalletInfo, WalletInfo } from '@cowprotocol/wallet' + +import { renderHook } from '@testing-library/react' + +import { TradeDerivedState, useDerivedTradeState } from 'modules/trade' + +import { useQuoteParamsRecipient } from './useQuoteParamsRecipient' +import { useTradeQuote } from './useTradeQuote' + +import { TradeQuoteState } from '../state/tradeQuoteAtom' + +// Mock dependencies +jest.mock('./useTradeQuote') +jest.mock('modules/trade', () => ({ + useDerivedTradeState: jest.fn(), +})) +jest.mock('@cowprotocol/wallet', () => ({ + useWalletInfo: jest.fn(), +})) + +const mockedUseTradeQuote = useTradeQuote as jest.MockedFunction +const mockedUseDerivedTradeState = useDerivedTradeState as jest.MockedFunction +const mockedUseWalletInfo = useWalletInfo as jest.MockedFunction + +const VALID_ADDRESS = '0x1234567890123456789012345678901234567890' +const ANOTHER_VALID_ADDRESS = '0x0987654321098765432109876543210987654321' +const ACCOUNT_ADDRESS = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' +const INVALID_ADDRESS = 'not-an-address' +const ENS_NAME = 'vitalik.eth' + +describe('useQuoteParamsRecipient', () => { + beforeEach(() => { + jest.clearAllMocks() + + // Default mock setup + mockedUseWalletInfo.mockReturnValue({ + account: ACCOUNT_ADDRESS, + } as unknown as WalletInfo) + }) + + describe('ReceiverAccountBridgeProvider', () => { + beforeEach(() => { + mockedUseTradeQuote.mockReturnValue({ + bridgeQuote: { + providerInfo: { + type: 'ReceiverAccountBridgeProvider', + }, + }, + } as unknown as TradeQuoteState) + }) + + it('should return recipient when it is a valid address', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: VALID_ADDRESS, + recipientAddress: undefined, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(VALID_ADDRESS) + }) + + it('should return recipientAddress when recipient is ENS name', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: ENS_NAME, + recipientAddress: ANOTHER_VALID_ADDRESS, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ANOTHER_VALID_ADDRESS) + }) + + it('should return account when recipient is invalid address', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: INVALID_ADDRESS, + recipientAddress: undefined, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ACCOUNT_ADDRESS) + }) + + it('should return account when recipient is not set', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: undefined, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ACCOUNT_ADDRESS) + }) + }) + + describe('Non-ReceiverAccountBridgeProvider', () => { + beforeEach(() => { + mockedUseTradeQuote.mockReturnValue({ + bridgeQuote: { + providerInfo: { + type: 'SomethingElseBridgeProvider', + }, + }, + } as unknown as TradeQuoteState) + }) + + it('should return recipientAddress when it is a valid address', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: VALID_ADDRESS, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(VALID_ADDRESS) + }) + + it('should return account when recipientAddress is undefined', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: undefined, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ACCOUNT_ADDRESS) + }) + + it('should return account when recipientAddress is invalid', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: INVALID_ADDRESS, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ACCOUNT_ADDRESS) + }) + + it('should ignore recipient field and use recipientAddress', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: VALID_ADDRESS, + recipientAddress: ANOTHER_VALID_ADDRESS, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ANOTHER_VALID_ADDRESS) + }) + }) + + describe('No bridge quote', () => { + beforeEach(() => { + mockedUseTradeQuote.mockReturnValue({ + bridgeQuote: undefined, + } as unknown as TradeQuoteState) + }) + + it('should return recipientAddress when it is a valid address', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: VALID_ADDRESS, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(VALID_ADDRESS) + }) + + it('should return account when recipientAddress is undefined', () => { + mockedUseDerivedTradeState.mockReturnValue({ + recipient: undefined, + recipientAddress: undefined, + } as unknown as TradeDerivedState) + + const { result } = renderHook(() => useQuoteParamsRecipient()) + + expect(result.current).toBe(ACCOUNT_ADDRESS) + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.ts new file mode 100644 index 0000000000..df6fd976be --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParamsRecipient.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react' + +import { isAddress } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useDerivedTradeState } from 'modules/trade' + +import { useTradeQuote } from './useTradeQuote' + +/** + * This hook is used in useQuoteParams() to calculate params of quote + * Changes of useQuoteParams() result trigger the quote fetching + * recipientAddress is present in state only when recipient is a ENS name + * recipient in state is what user typed in the custom recipient input + * For ReceiverAccountBridgeProvider we must trigger a new quote each time when custom recipient changes + */ +export function useQuoteParamsRecipient(): string | undefined { + const { bridgeQuote } = useTradeQuote() + const state = useDerivedTradeState() + const { account } = useWalletInfo() + + const { recipient, recipientAddress } = state || {} + const isReceiverAccountBridgeProvider = bridgeQuote?.providerInfo.type === 'ReceiverAccountBridgeProvider' + + return useMemo(() => { + if (isReceiverAccountBridgeProvider) { + if (recipient && isAddress(recipient)) { + return recipient + } + } + + return recipientAddress && isAddress(recipientAddress) ? recipientAddress : account + }, [isReceiverAccountBridgeProvider, account, recipient, recipientAddress]) +} diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts index 9c167e1d94..a526cd840c 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts @@ -75,6 +75,11 @@ const tradeQuotePollingParameters: TradeQuotePollingParameters = { useSuggestedSlippageApi: false, } +const mockTimings = { + now: Date.now(), + ref: { current: Date.now() }, +} + // eslint-disable-next-line max-lines-per-function describe('fetchAndProcessQuote', () => { let mockTradeQuoteManager: jest.Mocked @@ -165,6 +170,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.setLoading).toHaveBeenCalledWith(true) @@ -198,6 +204,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.setLoading).toHaveBeenCalledWith(true) @@ -239,6 +246,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockBridgingSdk.getQuote).toHaveBeenCalledWith(mockQuoteParams, { @@ -281,6 +289,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.onResponse).toHaveBeenCalledWith(mockQuoteAndPost, null, mockFetchParams) @@ -298,6 +307,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.onResponse).not.toHaveBeenCalled() @@ -320,6 +330,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.onError).toHaveBeenCalledWith( @@ -342,6 +353,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) // Should call onError for generic errors in swap quotes @@ -359,7 +371,7 @@ describe('fetchAndProcessQuote', () => { it('should handle successful bridge quote with onQuoteResult callback', async () => { const mockBridgeQuote: BridgeQuoteResults = { - providerInfo: { name: 'Test Provider', logoUrl: '', dappId: 'test', website: '' }, + providerInfo: { name: 'Test Provider', logoUrl: '', dappId: 'test', website: '', type: 'HookBridgeProvider' }, tradeParameters: crossChainQuoteParams, bridgeCallDetails: {} as any, amountsAndCosts: {} as any, @@ -394,6 +406,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) // Simulate the callback being called @@ -427,6 +440,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.onError).toHaveBeenCalledWith( @@ -453,6 +467,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(console.error).toHaveBeenCalledWith('[fetchAndProcessQuote]:: fetchQuote error', mockGenericError) @@ -467,6 +482,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) // Should not call any manager methods for null result @@ -484,6 +500,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(console.error).toHaveBeenCalledWith('[fetchAndProcessQuote]:: unexpected bridge error', unexpectedError) @@ -508,6 +525,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) // Simulate the callback being called with null quote @@ -540,6 +558,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) // Should not call any manager methods when request is cancelled @@ -569,6 +588,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockMapOperatorErrorToQuoteError).toHaveBeenCalledWith(mockErrorBody) @@ -592,6 +612,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockMapOperatorErrorToQuoteError).not.toHaveBeenCalled() @@ -618,6 +639,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, undefined, mockTradeQuoteManager, + mockTimings, ) expect(mockBridgingSdk.getQuote).toHaveBeenCalledWith(mockQuoteParams, { @@ -651,6 +673,7 @@ describe('fetchAndProcessQuote', () => { tradeQuotePollingParameters, mockAppData, mockTradeQuoteManager, + mockTimings, ) expect(mockTradeQuoteManager.setLoading).toHaveBeenCalledWith(false) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts index 05b6e7a76f..eeef4e9add 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts @@ -18,7 +18,7 @@ import { getIsOrderBookTypedError } from 'api/cowProtocol/getIsOrderBookTypedErr import { coWBFFClient } from 'common/services/bff' import { TradeQuoteManager } from '../hooks/useTradeQuoteManager' -import { TradeQuoteFetchParams, TradeQuotePollingParameters } from '../types' +import { QuotePollingUpdateTimings, TradeQuoteFetchParams, TradeQuotePollingParameters } from '../types' import { getBridgeQuoteSigner } from '../utils/getBridgeQuoteSigner' const getQuote = bridgingSdk.getQuote.bind(bridgingSdk) @@ -33,6 +33,7 @@ export async function fetchAndProcessQuote( { useSuggestedSlippageApi }: TradeQuotePollingParameters, appData: AppDataInfo['doc'] | undefined, tradeQuoteManager: TradeQuoteManager, + timings: QuotePollingUpdateTimings, ): Promise { const { hasParamsChanged, priceQuality } = fetchParams @@ -49,6 +50,9 @@ export async function fetchAndProcessQuote( } const processQuoteError = (error: Error): void => { + // Skip state update when another quote already started + if (timings.ref.current && timings.now !== timings.ref.current) return + const parsedError = parseError(error) console.error('[fetchAndProcessQuote]:: fetchQuote error', parsedError) @@ -71,7 +75,7 @@ export async function fetchAndProcessQuote( tradeQuoteManager.setLoading(hasParamsChanged) if (isBridge) { - await fetchBridgingQuote(fetchParams, quoteParams, advancedSettings, tradeQuoteManager, processQuoteError) + await fetchBridgingQuote(fetchParams, quoteParams, advancedSettings, tradeQuoteManager, processQuoteError, timings) } else { await fetchSwapQuote(fetchParams, quoteParams, advancedSettings, tradeQuoteManager, processQuoteError) } @@ -112,6 +116,7 @@ async function fetchBridgingQuote( advancedSettings: SwapAdvancedSettings, tradeQuoteManager: TradeQuoteManager, processQuoteError: (error: Error) => void, + timings: QuotePollingUpdateTimings, ): Promise { let isRequestCancelled = false @@ -143,6 +148,9 @@ async function fetchBridgingQuote( const error = data?.error if (error) { + // Skip state update when another quote already started + if (timings.ref.current && timings.now !== timings.ref.current) return + if (error instanceof BridgeProviderQuoteError) { tradeQuoteManager.onError(error, quoteParams.sellTokenChainId, quoteParams, fetchParams) return diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/types.ts b/apps/cowswap-frontend/src/modules/tradeQuote/types.ts index 2d78b08e0a..466801925b 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/types.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/types.ts @@ -11,3 +11,8 @@ export interface TradeQuotePollingParameters { isQuoteUpdatePossible: boolean useSuggestedSlippageApi: boolean } + +export interface QuotePollingUpdateTimings { + now: number + ref: { current: number | null } +} diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts index 6ba34daf2b..44e4c67f63 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts @@ -23,7 +23,7 @@ export function quoteUsingSameParameters( if (currentQuote.bridgeQuote) { const bridgeTradeParams = currentQuote.bridgeQuote.tradeParameters - const bridgePostHook = currentQuote.bridgeQuote.bridgeCallDetails?.preAuthorizedBridgingHook.postHook + const bridgePostHook = currentQuote.bridgeQuote.bridgeCallDetails?.preAuthorizedBridgingHook?.postHook const cases = [ compareAppDataWithoutQuoteData( diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts index ed65c97b1f..73f05ddb65 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/hooks/useHighFeeWarning.ts @@ -2,9 +2,10 @@ import { atom, useAtom } from 'jotai' import { SetStateAction, useMemo } from 'react' import { FEE_SIZE_THRESHOLD } from '@cowprotocol/common-const' -import { Fraction } from '@uniswap/sdk-core' +import { FractionUtils } from '@cowprotocol/common-utils' +import { Currency, CurrencyAmount, Fraction } from '@uniswap/sdk-core' -import { useGetReceiveAmountInfo } from 'modules/trade' +import { ReceiveAmountInfo, useGetReceiveAmountInfo } from 'modules/trade' import { useSafeEffect, useSafeMemo } from 'common/hooks/useSafeMemo' @@ -59,7 +60,7 @@ export function useHighFeeWarning(): UseHighFeeWarningReturn { const feePercentage = totalFeeAmount.divide(targetAmount).multiply(100).asFraction - const bridgeFeePercentage = bridgeFee?.amountInDestinationCurrency.divide(targetAmount).multiply(100).asFraction + const bridgeFeePercentage = getBridgeFeePercentage(bridgeFee, targetAmount) const isHighBridgeFee = Boolean(bridgeFeePercentage?.greaterThan(FEE_SIZE_THRESHOLD)) @@ -89,6 +90,27 @@ export function useHighFeeWarning(): UseHighFeeWarningReturn { ) } +function getBridgeFeePercentage( + bridgeFee: ReceiveAmountInfo['costs']['bridgeFee'], + targetAmount: CurrencyAmount, +): Fraction | undefined { + if (!bridgeFee) return undefined + + if (bridgeFee.amountInDestinationCurrency.currency.decimals !== targetAmount.currency.decimals) { + return bridgeFee.amountInDestinationCurrency + .divide( + FractionUtils.adjustDecimalsAtoms( + targetAmount, + targetAmount.currency.decimals, + bridgeFee.amountInDestinationCurrency.currency.decimals, + ), + ) + .multiply(100).asFraction + } + + return bridgeFee.amountInDestinationCurrency.divide(targetAmount).multiply(100).asFraction +} + function _computeFeeWarningAcceptedState({ feeWarningAccepted, isHighFee, diff --git a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx index c68e04a39a..60b0d9cc97 100644 --- a/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/TradeRateDetails/index.tsx @@ -10,7 +10,7 @@ import { useDerivedTradeState, NetworkCostsRow, useShouldPayGas, - useReceiveAmountInfo, + useGetReceiveAmountInfo, } from 'modules/trade' import { useTradeQuote } from 'modules/tradeQuote' import { useIsSlippageModified, useTradeSlippage } from 'modules/tradeSlippage' @@ -43,12 +43,11 @@ export function TradeRateDetails({ const slippage = useTradeSlippage() const isSlippageModified = useIsSlippageModified() - // todo replace by useGetReceiveAmountInfo when we decide what to show as bridge total fee - const receiveAmountInfo = useReceiveAmountInfo() + const receiveAmountInfo = useGetReceiveAmountInfo(true) const derivedTradeState = useDerivedTradeState() const tradeQuote = useTradeQuote() const shouldPayGas = useShouldPayGas() - const bridgeQuoteAmounts = useBridgeQuoteAmounts() + const bridgeQuoteAmounts = useBridgeQuoteAmounts(true) const inputCurrency = derivedTradeState?.inputCurrency @@ -80,7 +79,10 @@ export function TradeRateDetails({ ) } - const totalCosts = getTotalCosts(receiveAmountInfo, bridgeQuoteAmounts?.bridgeFee) + const totalCosts = getTotalCosts( + receiveAmountInfo, + bridgeQuoteAmounts?.bridgeFeeAmounts?.amountInIntermediateCurrency, + ) // Default expanded content if accordionContent prop is not supplied const defaultExpandedContent = ( diff --git a/apps/cowswap-frontend/src/modules/twap/state/scaledReceiveAmountInfoAtom.ts b/apps/cowswap-frontend/src/modules/twap/state/scaledReceiveAmountInfoAtom.ts deleted file mode 100644 index 46cc75b9a7..0000000000 --- a/apps/cowswap-frontend/src/modules/twap/state/scaledReceiveAmountInfoAtom.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { atom } from 'jotai' - -import { ReceiveAmountInfo, receiveAmountInfoAtom } from 'modules/trade' - -import { twapOrdersSettingsAtom } from './twapOrdersSettingsAtom' - -import { calculateTwapReceivedAmountInfo } from '../utils/calculateTwapReceivedAmountInfo' - -export const scaledReceiveAmountInfoAtom = atom((get) => { - const { numberOfPartsValue } = get(twapOrdersSettingsAtom) - const receiveAmountInfo = get(receiveAmountInfoAtom) - - return calculateTwapReceivedAmountInfo(receiveAmountInfo, numberOfPartsValue) -}) diff --git a/apps/cowswap-frontend/src/tradingSdk/TestReceiverAccountBridgeProvider.ts b/apps/cowswap-frontend/src/tradingSdk/TestReceiverAccountBridgeProvider.ts index 2367e4cec4..dc5eb612e4 100644 --- a/apps/cowswap-frontend/src/tradingSdk/TestReceiverAccountBridgeProvider.ts +++ b/apps/cowswap-frontend/src/tradingSdk/TestReceiverAccountBridgeProvider.ts @@ -1,4 +1,4 @@ -import { ChainInfo, EvmCall, TokenInfo } from '@cowprotocol/cow-sdk' +import { ChainInfo, EnrichedOrder, EvmCall, TokenInfo } from '@cowprotocol/cow-sdk' import { BridgeProvider, BridgeProviderInfo, @@ -12,8 +12,6 @@ import { ReceiverAccountBridgeProvider, } from '@cowprotocol/sdk-bridging' -import { getOrder } from 'api/cowProtocol' - /** * Test provider for testing the ReceiverAccountBridgeProvider using an account provided in the constructor. * It also relies on a bridge provider that will be used to get the quote, tokens, etc. (to implement this mock easily based on other working providers) @@ -26,6 +24,7 @@ export class TestReceiverAccountBridgeProvider implements ReceiverAccountBridgeP 'https://raw.githubusercontent.com/cowprotocol/cow-sdk/refs/heads/main/packages/bridging/src/providers/mock/mock-logo.webp', dappId: 'test-receiver-bridge', website: 'https://github.com/cowprotocol/cowswap', + type: 'ReceiverAccountBridgeProvider', } constructor( @@ -64,12 +63,12 @@ export class TestReceiverAccountBridgeProvider implements ReceiverAccountBridgeP async getBridgingParams( chainId: number, - orderUid: string, + order: EnrichedOrder, txHash: string, ): Promise<{ params: BridgingDepositParams; status: BridgeStatusResult } | null> { - console.log('getBridgingParams', chainId, orderUid, txHash) + const orderUid = order?.uid - const order = await getOrder(chainId, orderUid) + console.log('getBridgingParams', chainId, orderUid, txHash) if (!order) { return null diff --git a/apps/cowswap-frontend/src/tradingSdk/bridgingSdk.ts b/apps/cowswap-frontend/src/tradingSdk/bridgingSdk.ts index b6bad01e27..474c156d65 100644 --- a/apps/cowswap-frontend/src/tradingSdk/bridgingSdk.ts +++ b/apps/cowswap-frontend/src/tradingSdk/bridgingSdk.ts @@ -1,11 +1,10 @@ import { bungeeAffiliateCode } from '@cowprotocol/common-const' -import { isBarn, isDev, isProd, isProdLike, isStaging } from '@cowprotocol/common-utils' +import { isBarn, isDev, isProd, isStaging } from '@cowprotocol/common-utils' import { - AcrossBridgeProvider, - BridgeProvider, - BridgeQuoteResult, + NearIntentsBridgeProvider, BridgingSdk, BungeeBridgeProvider, + AcrossBridgeProvider, } from '@cowprotocol/sdk-bridging' import { orderBookApi } from 'cowSdk' @@ -14,7 +13,7 @@ import { tradingSdk } from './tradingSdk' const bungeeApiBase = getBungeeApiBase() -const bungeeBridgeProvider = new BungeeBridgeProvider({ +export const bungeeBridgeProvider = new BungeeBridgeProvider({ apiOptions: { includeBridges: ['across', 'cctp', 'gnosis-native-bridge'], apiBaseUrl: bungeeApiBase ? `${bungeeApiBase}/api/v1/bungee` : undefined, @@ -22,20 +21,19 @@ const bungeeBridgeProvider = new BungeeBridgeProvider({ affiliate: bungeeApiBase ? bungeeAffiliateCode : undefined, }, }) - -const acrossBridgeProvider = new AcrossBridgeProvider() -export const bridgeProviders: BridgeProvider[] = [bungeeBridgeProvider] - -// TODO: Should not enable Across on Prod until it's finalized -!isProdLike && bridgeProviders.push(acrossBridgeProvider) +export const acrossBridgeProvider = new AcrossBridgeProvider() +export const nearIntentsBridgeProvider = new NearIntentsBridgeProvider() export const bridgingSdk = new BridgingSdk({ - providers: bridgeProviders, + providers: [bungeeBridgeProvider, acrossBridgeProvider, nearIntentsBridgeProvider], enableLogging: false, tradingSdk, orderBookApi, }) +// Enable only Bungee by default +bridgingSdk.setAvailableProviders([bungeeBridgeProvider.info.dappId]) + function getBungeeApiBase(): string | undefined { if (isProd || isDev || isStaging || isBarn) { return 'https://backend.bungee.exchange' diff --git a/apps/cowswap-frontend/src/utils/__tests__/getExecutedSummaryData.test.ts b/apps/cowswap-frontend/src/utils/__tests__/getExecutedSummaryData.test.ts index f70c7b4d49..5c7afc3a59 100644 --- a/apps/cowswap-frontend/src/utils/__tests__/getExecutedSummaryData.test.ts +++ b/apps/cowswap-frontend/src/utils/__tests__/getExecutedSummaryData.test.ts @@ -151,7 +151,7 @@ describe('getExecutedSummaryDataWithSurplusToken', () => { expect(result.surplusToken.address).toBe(baseEth.address) }) - it('ignores override token when decimals do not align for sell orders', () => { + it('adjusts surplus from override token decimals when decimals do not align for sell orders', () => { const order = buildParsedOrder({ kind: OrderKind.SELL, inputToken: polygonAave, @@ -160,7 +160,7 @@ describe('getExecutedSummaryDataWithSurplusToken', () => { buyAmount: '1000000000000000000', executedSellAmount: '2000000000000000000', executedBuyAmount: '1050000000000000000', - surplusRaw: '50000000000000000', + surplusRaw: '50000', // 0.05 in 6 decimals (override token's decimals) }) const result = getExecutedSummaryDataWithSurplusToken(order, polygonUsdc) @@ -268,4 +268,29 @@ describe('getExecutedSummaryDataWithSurplusToken', () => { expect(result.surplusAmount.currency.address).toBe(polygonWeth.address) expect(result.surplusAmount.toExact()).toBe('0') }) + + it('handles bridge scenario with USDC (6 decimals) to WETH (18 decimals) correctly', () => { + const order = buildParsedOrder({ + kind: OrderKind.SELL, + inputToken: polygonUsdc, // 6 decimals + outputToken: baseWeth, // 18 decimals + sellAmount: '1000000', // 1 USDC + buyAmount: '500000000000000', // 0.0005 WETH + executedSellAmount: '1000000', + executedBuyAmount: '525000000000000', // 0.000525 WETH (5% surplus) + surplusRaw: '25000', // 0.025 USDC worth in 6 decimals + }) + + // Override token is polygonUsdc (the intermediate token with 6 decimals) + const result = getExecutedSummaryDataWithSurplusToken(order, polygonUsdc) + + // The output is baseWeth (18 decimals), but surplus is calculated in intermediate token (6 decimals) + expect(result.formattedSwappedAmount.currency.address).toBe(baseWeth.address) + expect(result.formattedSwappedAmount.currency.decimals).toBe(18) + + // Surplus should be adjusted from intermediate (6 decimals) to output (18 decimals) + expect(result.surplusAmount.currency.address).toBe(baseWeth.address) + expect(result.surplusAmount.currency.decimals).toBe(18) + expect(result.surplusAmount.toExact()).toBe('0.025') + }) }) diff --git a/apps/cowswap-frontend/src/utils/getExecutedSummaryData.ts b/apps/cowswap-frontend/src/utils/getExecutedSummaryData.ts index a02d9a7b4d..618a945697 100644 --- a/apps/cowswap-frontend/src/utils/getExecutedSummaryData.ts +++ b/apps/cowswap-frontend/src/utils/getExecutedSummaryData.ts @@ -1,4 +1,4 @@ -import { areAddressesEqual, isSellOrder } from '@cowprotocol/common-utils' +import { areAddressesEqual, FractionUtils, isSellOrder } from '@cowprotocol/common-utils' import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { BigNumber } from 'bignumber.js' @@ -66,18 +66,16 @@ export function getExecutedSummaryDataWithSurplusToken( const { surplusAmount: amount, surplusPercentage: percentage } = parsedOrder.executionData // Guard against missing surplus by falling back to '0' raw amount - const rawSurplus = amount - ? amount.decimalPlaces(0, BigNumber.ROUND_DOWN).toFixed(0) - : '0' + const rawSurplus = amount ? amount.decimalPlaces(0, BigNumber.ROUND_DOWN).toFixed(0) : '0' const surplusPercent = percentage?.multipliedBy(100)?.toFixed(2) - const { effectiveOutputToken, surplusDisplayToken } = resolveDisplayTokens({ + const { effectiveOutputToken, surplusDisplayToken, surplusAmount } = resolveDisplayTokens({ parsedOrder, parsedInputToken, parsedOutputToken, surplusToken, + rawSurplus, }) - const surplusAmount = CurrencyAmount.fromRawAmount(surplusDisplayToken, rawSurplus) const { formattedFilledAmount, formattedSwappedAmount, swappedAmountWithFee } = getFilledAmounts({ ...parsedOrder, @@ -100,6 +98,13 @@ interface ResolveDisplayTokensParams { parsedInputToken: Token parsedOutputToken: Token surplusToken: Token + rawSurplus: string +} + +interface DisplayTokenDetails { + effectiveOutputToken: Token + surplusDisplayToken: Token + surplusAmount: CurrencyAmount } function resolveDisplayTokens({ @@ -107,7 +112,8 @@ function resolveDisplayTokens({ parsedInputToken, parsedOutputToken, surplusToken, -}: ResolveDisplayTokensParams): { effectiveOutputToken: Token; surplusDisplayToken: Token } { + rawSurplus, +}: ResolveDisplayTokensParams): DisplayTokenDetails { const isSell = isSellOrder(parsedOrder.kind) const defaultSurplusToken = isSell ? parsedOutputToken : parsedInputToken @@ -115,6 +121,7 @@ function resolveDisplayTokens({ return { effectiveOutputToken: parsedOutputToken, surplusDisplayToken: defaultSurplusToken, + surplusAmount: CurrencyAmount.fromRawAmount(defaultSurplusToken, rawSurplus), } } @@ -130,11 +137,17 @@ function resolveDisplayTokens({ return { effectiveOutputToken: surplusToken, surplusDisplayToken: surplusToken, + surplusAmount: CurrencyAmount.fromRawAmount(surplusToken, rawSurplus), } } return { effectiveOutputToken: parsedOutputToken, surplusDisplayToken: defaultSurplusToken, + surplusAmount: FractionUtils.adjustDecimalsAtoms( + CurrencyAmount.fromRawAmount(defaultSurplusToken, rawSurplus), + surplusToken.decimals, + defaultSurplusToken.decimals, + ), } } diff --git a/apps/explorer/src/components/orders/BridgeDetailsTable/BridgeDetailsContent/contents.tsx b/apps/explorer/src/components/orders/BridgeDetailsTable/BridgeDetailsContent/contents.tsx index af327bb268..06d921fb5e 100644 --- a/apps/explorer/src/components/orders/BridgeDetailsTable/BridgeDetailsContent/contents.tsx +++ b/apps/explorer/src/components/orders/BridgeDetailsTable/BridgeDetailsContent/contents.tsx @@ -34,13 +34,13 @@ export function BridgingTime({ bridgeStatus: BridgeStatus fillTimeInSeconds: number | undefined }): ReactNode { - if (bridgeStatus !== BridgeStatus.IN_PROGRESS || fillTimeInSeconds === undefined) { + if (bridgeStatus !== BridgeStatus.IN_PROGRESS || !fillTimeInSeconds) { return null } return ( - {displayTime(fillTimeInSeconds, true)} + {displayTime(fillTimeInSeconds * 1000, true)} ) } diff --git a/apps/explorer/src/sdk/cowSdk.ts b/apps/explorer/src/sdk/cowSdk.ts index 8c45334279..9a98ffee8a 100644 --- a/apps/explorer/src/sdk/cowSdk.ts +++ b/apps/explorer/src/sdk/cowSdk.ts @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { bungeeAffiliateCode, getRpcProvider } from '@cowprotocol/common-const' import { isBarn, isDev, isProd, isStaging } from '@cowprotocol/common-utils' import { OrderBookApi, setGlobalAdapter, SupportedChainId } from '@cowprotocol/cow-sdk' -import { AcrossBridgeProvider, BungeeBridgeProvider } from '@cowprotocol/sdk-bridging' +import { AcrossBridgeProvider, BungeeBridgeProvider, NearIntentsBridgeProvider } from '@cowprotocol/sdk-bridging' import { EthersV5Adapter } from '@cowprotocol/sdk-ethers-v5-adapter' import { useNetworkId } from '../state/network' @@ -16,7 +16,7 @@ export const orderBookApi = new OrderBookApi() const bungeeApiBase = getBungeeApiBase() -export const bungeeBridgeProvider = new BungeeBridgeProvider({ +const bungeeBridgeProvider = new BungeeBridgeProvider({ apiOptions: { includeBridges: ['across', 'cctp', 'gnosis-native-bridge'], apiBaseUrl: bungeeApiBase ? `${bungeeApiBase}/api/v1/bungee` : undefined, @@ -25,9 +25,11 @@ export const bungeeBridgeProvider = new BungeeBridgeProvider({ }, }) -export const acrossBridgeProvider = new AcrossBridgeProvider() +const acrossBridgeProvider = new AcrossBridgeProvider() -export const knownBridgeProviders = [bungeeBridgeProvider, acrossBridgeProvider] +const nearIntentsBridgeProvider = new NearIntentsBridgeProvider() + +export const knownBridgeProviders = [bungeeBridgeProvider, acrossBridgeProvider, nearIntentsBridgeProvider] function getBungeeApiBase(): string | undefined { if (isProd || isDev || isStaging || isBarn) { diff --git a/apps/explorer/src/utils/getOrderBridgeProviderId.ts b/apps/explorer/src/utils/getOrderBridgeProviderId.ts index 143fbc3a7e..e0afc6ac47 100644 --- a/apps/explorer/src/utils/getOrderBridgeProviderId.ts +++ b/apps/explorer/src/utils/getOrderBridgeProviderId.ts @@ -8,6 +8,10 @@ export function getOrderBridgeProviderId(order: EnrichedOrder): string | undefin try { const appData = JSON.parse(order.fullAppData) as cowAppDataLatestScheme.AppDataRootSchema + const bridgeProviderId = appData.metadata.bridging?.providerId + + if (bridgeProviderId) return bridgeProviderId + const postHooks = appData.metadata.hooks?.post || [] const bridgeHooks = postHooks.filter((hook) => hook.dappId?.startsWith(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX)) diff --git a/libs/common-utils/src/fractionUtils.ts b/libs/common-utils/src/fractionUtils.ts index d68f895408..6ea7447839 100644 --- a/libs/common-utils/src/fractionUtils.ts +++ b/libs/common-utils/src/fractionUtils.ts @@ -125,17 +125,17 @@ export class FractionUtils { * For example, a fraction like 1.1/1 representing the price of USDC, DAI in units, will be turned into * 1.1/1000000000000 in atoms */ - static adjustDecimalsAtoms( - value: CurrencyAmount, + static adjustDecimalsAtoms( + value: CurrencyAmount, decimalsA: number, decimalsB: number, - ): CurrencyAmount - static adjustDecimalsAtoms(value: Fraction, decimalsA: number, decimalsB: number): Fraction - static adjustDecimalsAtoms( - value: Fraction | CurrencyAmount, + ): typeof value + static adjustDecimalsAtoms(value: Fraction, decimalsA: number, decimalsB: number): typeof value + static adjustDecimalsAtoms( + value: Fraction | CurrencyAmount, decimalsA: number, decimalsB: number, - ): Fraction | CurrencyAmount { + ): typeof value { if (decimalsA === decimalsB) { return value } diff --git a/libs/tokens/src/pure/TokenLogo/SingleLetterLogo.tsx b/libs/tokens/src/pure/TokenLogo/SingleLetterLogo.tsx index 8ab15df7e5..229bceb496 100644 --- a/libs/tokens/src/pure/TokenLogo/SingleLetterLogo.tsx +++ b/libs/tokens/src/pure/TokenLogo/SingleLetterLogo.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import { UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -17,10 +19,9 @@ export const SingleLetterLogoWrapper = styled.div` type SingleLetterLogoProps = { initial: string + address?: string } -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function SingleLetterLogo({ initial }: SingleLetterLogoProps) { - return {initial} +export function SingleLetterLogo({ initial, address }: SingleLetterLogoProps): ReactNode { + return {initial} } diff --git a/libs/tokens/src/pure/TokenLogo/index.tsx b/libs/tokens/src/pure/TokenLogo/index.tsx index 7f49b5e345..d7808c1681 100644 --- a/libs/tokens/src/pure/TokenLogo/index.tsx +++ b/libs/tokens/src/pure/TokenLogo/index.tsx @@ -1,5 +1,5 @@ import { atom, useAtom } from 'jotai' -import { useCallback, useMemo } from 'react' +import { ReactNode, useCallback, useMemo } from 'react' import { BaseChainInfo, @@ -42,9 +42,8 @@ export interface TokenLogoProps { } // TODO: Break down this large function into smaller functions -// TODO: Add proper return type annotation // TODO: Reduce function complexity by extracting logic -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, complexity, max-lines-per-function +// eslint-disable-next-line complexity, max-lines-per-function export function TokenLogo({ logoURI, token, @@ -53,7 +52,7 @@ export function TokenLogo({ sizeMobile, noWrap, hideNetworkBadge, -}: TokenLogoProps) { +}: TokenLogoProps): ReactNode { const tokensByAddress = useTokensByAddressMap() const [invalidUrls, setInvalidUrls] = useAtom(invalidUrlsAtom) @@ -78,9 +77,8 @@ export function TokenLogo({ const currentUrl = validUrls?.[0] - - const logoUrl = useNetworkLogo(token?.chainId) - const showNetworkBadge = logoUrl && !hideNetworkBadge + const networkLogoUrl = useNetworkLogo(token?.chainId) + const showNetworkBadge = networkLogoUrl && !hideNetworkBadge const onError = useCallback(() => { if (!currentUrl) return @@ -105,17 +103,20 @@ export function TokenLogo({ ) } + const address = token && 'address' in token ? token.address : '' + const actualTokenContent = currentUrl ? ( {`${token?.symbol ) : initial ? ( - + ) : ( @@ -137,7 +138,12 @@ export function TokenLogo({ const cutThicknessForCalc = getBorderWidth(chainLogoSizeForCalc) return ( - + <> {showNetworkBadge ? ( - {`${chainName} + {`${chainName} )} diff --git a/libs/types/src/bridge.ts b/libs/types/src/bridge.ts index eaa2282da3..9b7bd25812 100644 --- a/libs/types/src/bridge.ts +++ b/libs/types/src/bridge.ts @@ -9,6 +9,10 @@ export interface BridgeQuoteAmounts> { swapMinReceiveAmount: Amount // that should be moved on bridge (before sending to user) bridgeMinReceiveAmount: Amount // that should be moved to user bridgeFee: Amount + bridgeFeeAmounts?: { + amountInIntermediateCurrency: Amount + amountInDestinationCurrency: Amount + } } export interface BridgeOrderData { diff --git a/package.json b/package.json index 076747729f..d3964b2158 100644 --- a/package.json +++ b/package.json @@ -78,12 +78,12 @@ "@coinbase/wallet-sdk": "^3.3.0", "@cowprotocol/cms": "^0.11.0", "@cowprotocol/cow-runner-game": "^0.2.9", - "@cowprotocol/cow-sdk": "^7.1.0", - "@cowprotocol/sdk-bridging": "^0.6.0", - "@cowprotocol/sdk-composable": "^0.1.10", - "@cowprotocol/sdk-cow-shed": "^0.1.10", - "@cowprotocol/sdk-ethers-v5-adapter": "^0.2.0", - "@cowprotocol/sdk-subgraph": "^0.2.0", + "@cowprotocol/cow-sdk": "npm:@cowprotocol/cow-sdk@pr-679", + "@cowprotocol/sdk-bridging": "npm:@cowprotocol/sdk-bridging@pr-679", + "@cowprotocol/sdk-composable": "npm:@cowprotocol/sdk-composable@pr-679", + "@cowprotocol/sdk-cow-shed": "npm:@cowprotocol/sdk-cow-shed@pr-679", + "@cowprotocol/sdk-ethers-v5-adapter": "npm:@cowprotocol/sdk-ethers-v5-adapter@pr-679", + "@cowprotocol/sdk-subgraph": "npm:@cowprotocol/sdk-subgraph@pr-679", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@ethersproject/experimental": "^5.8.0", diff --git a/yarn.lock b/yarn.lock index 36b48ea98f..b448abe8f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1812,145 +1812,147 @@ resolved "https://registry.yarnpkg.com/@cowprotocol/cow-runner-game/-/cow-runner-game-0.2.9.tgz#3f94b3f370bd114f77db8b1d238cba3ef4e9d644" integrity sha512-rX7HnoV+HYEEkBaqVUsAkGGo0oBrExi+d6Io+8nQZYwZk+IYLmS9jdcIObsLviM2h4YX8+iin6NuKl35AaiHmg== -"@cowprotocol/cow-sdk@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-7.1.0.tgz#8f327e61d97d51fdbd97500cee91226548fbdd69" - integrity sha512-T26cWE47pr3Z+RQNEzqPLsXy8a/uBulE321FUGREnhpraGlkWn8m68ggutuNXHv9sXsHJBv/8neKSFnROVUqiA== - dependencies: - "@cowprotocol/sdk-app-data" "4.1.6" - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" - "@cowprotocol/sdk-order-book" "0.2.0" - "@cowprotocol/sdk-order-signing" "0.1.10" - "@cowprotocol/sdk-trading" "0.4.5" - -"@cowprotocol/sdk-app-data@4.1.6": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-app-data/-/sdk-app-data-4.1.6.tgz#e4d5b7843fe875e8a91da48a8fa3a0045b3b778e" - integrity sha512-wPy0p1HibKKBjGrH9iRYDGGYIzIJQORRAdy/IB+PRojBLTeBEkTp9HKvNHwDaPhdAlV19e/U5p65WM4b5sKW5A== - dependencies: - "@cowprotocol/sdk-common" "0.3.0" +"@cowprotocol/cow-sdk@npm:@cowprotocol/cow-sdk@pr-679": + version "7.1.1-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/cow-sdk/7.1.1-pr-679-806017b0.0/e937e6c95a1598e437d5bbd213135004ada2dddd#e937e6c95a1598e437d5bbd213135004ada2dddd" + integrity sha512-hH/wBPkqmOMo46EMzssFQtXuUnQHNeuW98neaGxEGEM8VaTdtNcAf25k3DahUAhwvuUuQXG9GNoQglawOmzfrg== + dependencies: + "@cowprotocol/sdk-app-data" "4.1.6-pr-679-806017b0.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-book" "0.2.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-signing" "0.1.11-pr-679-806017b0.0" + "@cowprotocol/sdk-trading" "0.4.6-pr-679-806017b0.0" + +"@cowprotocol/sdk-app-data@4.1.6-pr-679-806017b0.0": + version "4.1.6-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-app-data/4.1.6-pr-679-806017b0.0/662d7408546ad8d48019d4edbec988490a832a90#662d7408546ad8d48019d4edbec988490a832a90" + integrity sha512-ycFxN1X6Drod2jAjKvgW5+TthWk04w6RyPMpnUbzTUAvbaaaHO4nkZNe/Iw8qXGrkcqvAC338sIkAl1r8OB1AQ== + dependencies: + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" ajv "^8.11.0" cross-fetch "^3.1.5" ipfs-only-hash "^4.0.0" json-stringify-deterministic "^1.0.8" multiformats "^9.6.4" -"@cowprotocol/sdk-bridging@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-bridging/-/sdk-bridging-0.6.0.tgz#b5697e2eda3d494cd610431f42c3a153c4cb5b47" - integrity sha512-P3HC422mYnBfejGsVf0KlpApD7W6zGhs9HBWLymH6a5c0jMv3hpIuf7X9aCU5RbxShSWQwGQV0o8toZGb8iSGg== - dependencies: - "@cowprotocol/sdk-app-data" "4.1.6" - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" - "@cowprotocol/sdk-cow-shed" "0.1.10" - "@cowprotocol/sdk-order-book" "0.2.0" - "@cowprotocol/sdk-trading" "0.4.5" - "@cowprotocol/sdk-weiroll" "0.1.5" - -"@cowprotocol/sdk-common@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-common/-/sdk-common-0.3.0.tgz#f03a5e36d67d0c5cdb6be5bd7ceda879b751542d" - integrity sha512-xk2VUjO4+XI5968r1pYFbqUxM2nmBcVPvGIw0pcqDpx4OLef0Flr3tuHtZY42RYYa6nNsNKOBV9Jd4RauYUOgQ== - -"@cowprotocol/sdk-composable@^0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-composable/-/sdk-composable-0.1.10.tgz#262eb90888048db4a7599d4a668827b259aff015" - integrity sha512-lX/ik6manAdNQRtOAAG2P9NcX69en6tWSG2yMwEnNWufP4TNVs1BNSxcpBWCvK0oEUwkuPG9VKLKYvY3WVBUzw== - dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" - "@cowprotocol/sdk-order-book" "0.2.0" - "@cowprotocol/sdk-order-signing" "0.1.10" +"@cowprotocol/sdk-bridging@npm:@cowprotocol/sdk-bridging@pr-679": + version "0.6.2-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-bridging/0.6.2-pr-679-806017b0.0/8d9e6abf774143620d71b03621e056eda0b7eed7#8d9e6abf774143620d71b03621e056eda0b7eed7" + integrity sha512-8PN5KFJ1roe35gC/7SRyGqDv4l5VxuVXYVPx+UWEMMVSEwNaULYKY4S8Sydud2iptQPJLHUuK4d21t61cDLmGA== + dependencies: + "@cowprotocol/sdk-app-data" "4.1.6-pr-679-806017b0.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" + "@cowprotocol/sdk-cow-shed" "0.2.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-book" "0.2.0-pr-679-806017b0.0" + "@cowprotocol/sdk-trading" "0.4.6-pr-679-806017b0.0" + "@cowprotocol/sdk-weiroll" "0.1.5-pr-679-806017b0.0" + "@defuse-protocol/one-click-sdk-typescript" "0.1.1-0.2" + json-stable-stringify "^1.3.0" + +"@cowprotocol/sdk-common@0.3.0-pr-679-806017b0.0": + version "0.3.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-common/0.3.0-pr-679-806017b0.0/34994cd6214cad583eed2c1e608955f14a5f696c#34994cd6214cad583eed2c1e608955f14a5f696c" + integrity sha512-7X+B0sC5/ppUc/6vxhR3KcVjiwzxFNnhYWIzXhWTC/4v0EkTzaAy9GLAywwM812cLYvpMwIrv4XR0d8rn8KUtw== + +"@cowprotocol/sdk-composable@npm:@cowprotocol/sdk-composable@pr-679": + version "0.1.11-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-composable/0.1.11-pr-679-806017b0.0/da9eda4898b3cdf012dc8a85d7aab14add7cb8e4#da9eda4898b3cdf012dc8a85d7aab14add7cb8e4" + integrity sha512-ufyA+LhgYEWB8fEaPBTDhDTJO3NbW6U7Cfg8/WjcQf0x/dgaSVfGCiJ2U1oiQJKX7fRRaazNZS48SlW2SU+pnQ== + dependencies: + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-book" "0.2.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-signing" "0.1.11-pr-679-806017b0.0" "@openzeppelin/merkle-tree" "^1.0.8" -"@cowprotocol/sdk-config@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-config/-/sdk-config-0.3.0.tgz#8a0e8e81daf3e82c60f71ff73893a794dd08c589" - integrity sha512-EUDqXMSYsgLdXDnvC9sc9ozpsjQdP6R738R0kKNWp/mTI9j7wsI9sVrDcKIoX7qH+pu0aRmRxEFbEbW7uXUcIg== +"@cowprotocol/sdk-config@0.3.0-pr-679-806017b0.0": + version "0.3.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-config/0.3.0-pr-679-806017b0.0/d344b01f801cbc9541209083d5a2aa5d27f7b6c0#d344b01f801cbc9541209083d5a2aa5d27f7b6c0" + integrity sha512-CLhBmawt0Gs1nAxd3eprHQaYY/RphbvPSe/gF2LGYrgUbanGvKWAy13RpywHNMJiwwQPKX50lBVRtrso8GbJkQ== dependencies: exponential-backoff "^3.1.1" limiter "^2.1.0" -"@cowprotocol/sdk-contracts-ts@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-contracts-ts/-/sdk-contracts-ts-0.4.4.tgz#28084c697a88ff4e756bdfa3d85a2c187ee5cb3f" - integrity sha512-lZA1P554EXtgu1+SCw9aj/BCUZpaKWuoaJB3iiuAFw/sq9Ax407L5T1v2+sj+hhLy49EjiVNjoZxzC7wVXmOKA== +"@cowprotocol/sdk-contracts-ts@0.5.0-pr-679-806017b0.0": + version "0.5.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-contracts-ts/0.5.0-pr-679-806017b0.0/25df47b4f43b8d4ae55535b8e595f1c316d735b5#25df47b4f43b8d4ae55535b8e595f1c316d735b5" + integrity sha512-iAeqcjF+D1gL8S1HMIUMi7MA/mssCUAC3dM0oBul+TJJA7ho8Bbe36HyxV282IulBgcNUYKbEtS6GHDUD5e/EQ== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" -"@cowprotocol/sdk-cow-shed@0.1.10", "@cowprotocol/sdk-cow-shed@^0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-cow-shed/-/sdk-cow-shed-0.1.10.tgz#4776bd6fe0b442bd7b5c458d5876cbfdd81be21d" - integrity sha512-GmRIYXTjg0bPKTnM1HXlQu+3JvCaO04gt6si/tItZOUG6mrBxlYzFe77mEKHohJ2kGpOuehr9PF+z/0prHrcrg== +"@cowprotocol/sdk-cow-shed@0.2.0-pr-679-806017b0.0", "@cowprotocol/sdk-cow-shed@npm:@cowprotocol/sdk-cow-shed@pr-679": + version "0.2.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-cow-shed/0.2.0-pr-679-806017b0.0/1bb93af02ee62ebae6d0cd64c22f01cd4b53f3cc#1bb93af02ee62ebae6d0cd64c22f01cd4b53f3cc" + integrity sha512-ByhREsNrheWUbNbrNAkUkpkD6nzqJhey9JIEpdwZVYSs2wefKL7EcaXVWhIcivw7HGU/N47TsOXF1PD03XWXGw== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" -"@cowprotocol/sdk-ethers-v5-adapter@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-ethers-v5-adapter/-/sdk-ethers-v5-adapter-0.2.0.tgz#c04ea01fe38635a4afc77ad84078db3987999628" - integrity sha512-90YxjDHe6WHwa6NDxXn04KFj71TAQyfuO8T1SI8eWgNTaF9EDEgMrTXmfdBDp7S9WZSBVfyu3XQGOoXe3v4PGQ== +"@cowprotocol/sdk-ethers-v5-adapter@npm:@cowprotocol/sdk-ethers-v5-adapter@pr-679": + version "0.2.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-ethers-v5-adapter/0.2.0-pr-679-806017b0.0/221464313049e27d1e68369f4e995a9bab5c3350#221464313049e27d1e68369f4e995a9bab5c3350" + integrity sha512-I1r+FNN1oW8fwUQXPLE20UiolX7pim5BZ9kD3qhDTOZOKu6cFbPWG3FQyRYadkN7h1FU6i04T5y7eWOeugmhOw== dependencies: - "@cowprotocol/sdk-common" "0.3.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" -"@cowprotocol/sdk-order-book@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-book/-/sdk-order-book-0.2.0.tgz#b4c24baa50c9c9670e951d0b9059dfd66ff7ed52" - integrity sha512-OeSHMGVEx7LCX+7ZnoRwLdIkJX25/tiquCVaocagrqzyPPPxD/TOg6BGLjgvl0kzoXLtfoSQ3CSz3IfvN/Etlg== +"@cowprotocol/sdk-order-book@0.2.0-pr-679-806017b0.0": + version "0.2.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-order-book/0.2.0-pr-679-806017b0.0/37c000f148ff606ab60f354350d03ea46ce64667#37c000f148ff606ab60f354350d03ea46ce64667" + integrity sha512-WekxikF38yTMotTss/WdOLHnoL4awlIgT2u7iSefonj/zEilZMpEMU1hsiEnrpUmL8UR5F7Dix7RUR5X9ckU+A== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" cross-fetch "^3.2.0" exponential-backoff "^3.1.2" limiter "^3.0.0" -"@cowprotocol/sdk-order-signing@0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-signing/-/sdk-order-signing-0.1.10.tgz#2526f05229cda4476c2ff2f6b446789cc12d78da" - integrity sha512-yDiOLAdguJAp/VEFWBBft0FyRZXTyAe7Ic9fzkILN5BG7u+NkGzvN8Kgz/1Fr2OtE4lixfp6S0Mh744kuamvLg== +"@cowprotocol/sdk-order-signing@0.1.11-pr-679-806017b0.0": + version "0.1.11-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-order-signing/0.1.11-pr-679-806017b0.0/a5a927208db0fa115341cf9bb1b95a8fc0e28879#a5a927208db0fa115341cf9bb1b95a8fc0e28879" + integrity sha512-nEjTSjqlTZtTD+2qiUiRfuG4ljqZFLO22IZUPjjEgOKboOGdvmRKBX87aEGg7ZhmrQwZbV4PHHLjOvn2WpHPiA== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" - "@cowprotocol/sdk-order-book" "0.2.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-book" "0.2.0-pr-679-806017b0.0" -"@cowprotocol/sdk-subgraph@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-subgraph/-/sdk-subgraph-0.2.0.tgz#66a6abaf2fed0d7096cab018709a5eaff26e81cc" - integrity sha512-PHZcmW/Jx6BgfUuAY1Oo/11XCfNVaOogGmr2sJ3HSA6c0+TEZmSzrSBtLDxDnaQC2i1y0BL0M9kyCNlxO5XntQ== +"@cowprotocol/sdk-subgraph@npm:@cowprotocol/sdk-subgraph@pr-679": + version "0.2.0-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-subgraph/0.2.0-pr-679-806017b0.0/71d2546aa534d018edd5b8a254c845dbdbe751f2#71d2546aa534d018edd5b8a254c845dbdbe751f2" + integrity sha512-G1FlWUYYnodWda60jua8awmbpLlEQPS14i54n7gA/SxC26MLcLRiIKIznc44jmXTcQJk98KG7zw3Hr2CuuzBlw== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" graphql "^16.11.0" graphql-request "^4.3.0" -"@cowprotocol/sdk-trading@0.4.5": - version "0.4.5" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-trading/-/sdk-trading-0.4.5.tgz#d4979e6642edbdea5ec97a71f686d2f2447fe250" - integrity sha512-bBGr8+X4IlyS7v9uDalWhj1qJ71OciHkq1iVohOJdgEOO7OSX+ZUT14qcERtaRzYDDx3WMYlqjlTUKAtvo7nsA== - dependencies: - "@cowprotocol/sdk-app-data" "4.1.6" - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" - "@cowprotocol/sdk-contracts-ts" "0.4.4" - "@cowprotocol/sdk-order-book" "0.2.0" - "@cowprotocol/sdk-order-signing" "0.1.10" +"@cowprotocol/sdk-trading@0.4.6-pr-679-806017b0.0": + version "0.4.6-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-trading/0.4.6-pr-679-806017b0.0/4a41dbef561dc31d4aa200523a01a5916568a12a#4a41dbef561dc31d4aa200523a01a5916568a12a" + integrity sha512-MG3FQyfIVhyRnZvAkjzGCckMFKdBW5vhZxfJr4fE6BZ6ie4ZjtkYZ5TsIz7MTOr5D3G3T/x/L/UBdRAV1P2D4g== + dependencies: + "@cowprotocol/sdk-app-data" "4.1.6-pr-679-806017b0.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-contracts-ts" "0.5.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-book" "0.2.0-pr-679-806017b0.0" + "@cowprotocol/sdk-order-signing" "0.1.11-pr-679-806017b0.0" deepmerge "^4.3.1" -"@cowprotocol/sdk-weiroll@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-weiroll/-/sdk-weiroll-0.1.5.tgz#e63bebab271b7c89bcbdc89047e092223a871d73" - integrity sha512-O8FTFaI6ZDogLHC7ruK19ggVrQa0CmWPMloeRwvRR2y7dp2nwzZjmyQOSuQs10AFIWr3yVIW1tH9kn/aoUTwEQ== +"@cowprotocol/sdk-weiroll@0.1.5-pr-679-806017b0.0": + version "0.1.5-pr-679-806017b0.0" + resolved "https://npm.pkg.github.com/download/@cowprotocol/sdk-weiroll/0.1.5-pr-679-806017b0.0/0145b875c76d52d0dc842de429467d663a49e4c1#0145b875c76d52d0dc842de429467d663a49e4c1" + integrity sha512-Hsv2QQvXau3bvxQ3tJ8QKmp7t+JLBwurSwq0SFzsTb0TWjjDB4hxqRkdLP1C/HEFNRHHeD/87+VEeJT5cMi37Q== dependencies: - "@cowprotocol/sdk-common" "0.3.0" - "@cowprotocol/sdk-config" "0.3.0" + "@cowprotocol/sdk-common" "0.3.0-pr-679-806017b0.0" + "@cowprotocol/sdk-config" "0.3.0-pr-679-806017b0.0" "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -2168,6 +2170,14 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@defuse-protocol/one-click-sdk-typescript@0.1.1-0.2": + version "0.1.1-0.2" + resolved "https://registry.yarnpkg.com/@defuse-protocol/one-click-sdk-typescript/-/one-click-sdk-typescript-0.1.1-0.2.tgz#ffab6f7bc90b8514273ac8c9183c0b271ce22a09" + integrity sha512-Jgt8uZlB5hQAo3UpyHH9XcXKNT6Vsqd7TTPy/vLEwuOvQ88Pag9qUxpU9Z2jYMD8SqOpxzaJrtgx+FSDb4lQ9A== + dependencies: + axios "^1.6.8" + form-data "^4.0.0" + "@ecies/ciphers@^0.2.1": version "0.2.2" resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.2.tgz#82a15b10a6e502b63fb30915d944b2eaf3ff17ff" @@ -12021,6 +12031,15 @@ axios@^1.11.0, axios@^1.12.0: form-data "^4.0.4" proxy-from-env "^1.1.0" +axios@^1.6.8: + version "1.13.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" + integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + axobject-query@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" @@ -17928,7 +17947,7 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.4, form-data@~4.0.4: +form-data@^4.0.0, form-data@^4.0.4, form-data@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== @@ -22051,6 +22070,17 @@ json-stable-stringify@^1.0.2: dependencies: jsonify "^0.0.1" +json-stable-stringify@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-deterministic@^1.0.12, json-stringify-deterministic@^1.0.8: version "1.0.12" resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz#aaa3f907466ed01e3afd77b898d0a2b3b132820a"