diff --git a/packages/sdk-router/README.md b/packages/sdk-router/README.md index 665b28a9fa..186c6b525b 100644 --- a/packages/sdk-router/README.md +++ b/packages/sdk-router/README.md @@ -317,3 +317,14 @@ transfer on HyperEVM. `sdk.synModuleSet.getBridgeDeliveryChainId(destChainId, tx returns the verified destination (1337 for Core submission, 999 for fallback), or `undefined` while pending. The existing boolean status reports completion of either outcome. Core submission does not independently verify the subsequent balance update. + +### HyperCore USDC deposits + +`bridgeV2` requests Relay deposits to HyperCore Perps from Relay-supported EVM +origins, including HyperEVM. Pass `HYPERCORE_CHAIN_ID` (1337) and +`HYPERCORE_USDC_ADDRESS` +(`0x00000000000000000000000000000000`) as the destination; no HyperCore RPC +provider is needed. Output amounts use 8 decimals. Relay receives the selected +source token as the origin currency and quotes any conversion needed. A route is +available when Relay returns a usable quote. `toRecipient` sets the credited +HyperCore address. Track completion through the `Relay` module. diff --git a/packages/sdk-router/src/constants/addresses.ts b/packages/sdk-router/src/constants/addresses.ts index 152ea0f713..d67914ed25 100644 --- a/packages/sdk-router/src/constants/addresses.ts +++ b/packages/sdk-router/src/constants/addresses.ts @@ -99,6 +99,9 @@ export const RELAY_ADDRESS_MAP: AddressMap = generateAddressMap( RELAY_EXCEPTION_MAP ) +// Relay's native USDC currency identifier on HyperCore (Perps). +export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000' + /** * USDT0 contract address for supported chains. * Note: for Ethereum this is actually native USDT. diff --git a/packages/sdk-router/src/relay/relayModuleSet.test.ts b/packages/sdk-router/src/relay/relayModuleSet.test.ts new file mode 100644 index 0000000000..f3533aa362 --- /dev/null +++ b/packages/sdk-router/src/relay/relayModuleSet.test.ts @@ -0,0 +1,181 @@ +import { BigNumber, providers } from 'ethers' + +import { + HYPERCORE_CHAIN_ID, + HYPERCORE_USDC_ADDRESS, + SupportedChainId, + TOKEN_ZAP_V1_ADDRESS_MAP, +} from '../constants' +import { EngineID, decodeZapData, getEmptyRoute } from '../swap' +import { TokenMetadataFetcher } from '../utils' +import * as relayApi from './api' +import { RelayModuleSet } from './relayModuleSet' + +const base = 8453 +const sourceToken = '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb' +const sender = '0x1111111111111111111111111111111111111111' +const recipient = '0x2222222222222222222222222222222222222222' +const relayDeposit = '0x4cd00e387622c35bddb9b4c962c136462338bc31' +const relayCalldata = '0xe8017952' + +const setup = () => + new RelayModuleSet( + [base, 1, SupportedChainId.HYPEREVM].map((chainId) => ({ + chainId, + provider: new providers.StaticJsonRpcProvider( + 'http://localhost:1', + chainId + ), + })) + ) + +afterEach(() => jest.restoreAllMocks()) + +it('passes the selected source token to Relay for HyperCore and other destinations', async () => { + const relay = setup() + const coreCandidates = await relay.getBridgeTokenCandidates({ + fromChainId: base, + toChainId: HYPERCORE_CHAIN_ID, + fromToken: sourceToken, + toToken: HYPERCORE_USDC_ADDRESS, + }) + expect(coreCandidates).toEqual([ + { + originChainId: base, + destChainId: HYPERCORE_CHAIN_ID, + originToken: sourceToken, + destToken: HYPERCORE_USDC_ADDRESS, + }, + ]) + expect( + await relay.getBridgeTokenCandidates({ + fromChainId: base, + toChainId: HYPERCORE_CHAIN_ID, + fromToken: sourceToken, + toToken: sourceToken, + }) + ).toEqual([]) + expect( + await relay.getBridgeTokenCandidates({ + fromChainId: base, + toChainId: 1, + fromToken: sourceToken, + }) + ).toEqual([expect.objectContaining({ originToken: sourceToken })]) + expect( + await relay.getBridgeTokenCandidates({ + fromChainId: SupportedChainId.HYPEREVM, + toChainId: HYPERCORE_CHAIN_ID, + fromToken: '0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb', + toToken: HYPERCORE_USDC_ADDRESS, + }) + ).toEqual([ + expect.objectContaining({ + originChainId: SupportedChainId.HYPEREVM, + destChainId: HYPERCORE_CHAIN_ID, + originToken: '0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb', + }), + ]) +}) + +it('quotes one HyperCore deposit step for the requested recipient', async () => { + const relay = setup() + const [bridgeToken] = await relay.getBridgeTokenCandidates({ + fromChainId: base, + toChainId: HYPERCORE_CHAIN_ID, + fromToken: sourceToken, + toToken: HYPERCORE_USDC_ADDRESS, + }) + const quoteSpy = jest.spyOn(relayApi, 'getQuote').mockResolvedValue({ + steps: [ + { + id: 'deposit', + kind: 'transaction', + items: [ + { + data: { + from: TOKEN_ZAP_V1_ADDRESS_MAP[base], + to: relayDeposit, + data: relayCalldata, + value: '0', + chainId: base, + }, + }, + ], + }, + ], + requestId: 'test', + details: { + currencyIn: {} as relayApi.QuoteResponse['details']['currencyIn'], + currencyOut: { + currency: { + chainId: HYPERCORE_CHAIN_ID, + address: HYPERCORE_USDC_ADDRESS, + symbol: 'USDC', + name: 'USDC (Perps)', + decimals: 8, + }, + amount: '9996999900', + amountFormatted: '99.969999', + amountUsd: '99.969999', + minimumAmount: '9987002900', + }, + timeEstimate: 60, + }, + } as relayApi.QuoteResponse) + const originSwapRoute = { + ...getEmptyRoute(EngineID.NoOp), + chainId: base, + fromToken: sourceToken, + toToken: bridgeToken.originToken, + fromAmount: BigNumber.from('100000000000000000000'), + expectedToAmount: BigNumber.from('100000000000000000000'), + } + const route = await relay.getBridgeRouteV2({ + bridgeToken, + originSwapRoute, + toToken: HYPERCORE_USDC_ADDRESS, + fromSender: sender, + toRecipient: recipient, + }) + expect(quoteSpy).toHaveBeenCalledWith( + expect.objectContaining({ + user: TOKEN_ZAP_V1_ADDRESS_MAP[base], + originCurrency: bridgeToken.originToken, + destinationChainId: HYPERCORE_CHAIN_ID, + destinationCurrency: HYPERCORE_USDC_ADDRESS, + recipient, + refundTo: sender, + useReceiver: true, + explicitDeposit: true, + usePermit: false, + }) + ) + expect(route?.expectedToAmount.toString()).toBe('9996999900') + expect(route?.minToAmount.toString()).toBe('9987002900') + expect(decodeZapData(route?.zapData ?? '0x')).toEqual( + expect.objectContaining({ + target: relayDeposit, + payload: relayCalldata, + }) + ) +}) + +it('uses Relay status and 8-decimal native USDC without a HyperCore provider', async () => { + const relay = setup() + expect(relay.getModule(HYPERCORE_CHAIN_ID)).toBeDefined() + const fetcher = new TokenMetadataFetcher({}) + expect( + await fetcher.getTokenDecimals(HYPERCORE_CHAIN_ID, HYPERCORE_USDC_ADDRESS) + ).toBe(8) + jest.spyOn(relayApi, 'getRequests').mockResolvedValue({ + requests: [ + { + id: 'test', + status: relayApi.Status.Success, + data: { inTxs: [{ hash: '0x1234' }] }, + }, + ], + }) + expect(await relay.getBridgeTxStatus(HYPERCORE_CHAIN_ID, '0x1234')).toBe(true) +}) diff --git a/packages/sdk-router/src/relay/relayModuleSet.ts b/packages/sdk-router/src/relay/relayModuleSet.ts index cb9d5602d6..9c9dbdd74c 100644 --- a/packages/sdk-router/src/relay/relayModuleSet.ts +++ b/packages/sdk-router/src/relay/relayModuleSet.ts @@ -1,7 +1,12 @@ -import { Zero } from '@ethersproject/constants' +import { AddressZero, Zero } from '@ethersproject/constants' import { BigNumber } from 'ethers' -import { RELAY_ADDRESS_MAP, TOKEN_ZAP_V1_ADDRESS_MAP } from '../constants' +import { + HYPERCORE_CHAIN_ID, + HYPERCORE_USDC_ADDRESS, + RELAY_ADDRESS_MAP, + TOKEN_ZAP_V1_ADDRESS_MAP, +} from '../constants' import { BridgeRoute, BridgeRouteV2, @@ -18,6 +23,7 @@ import { ChainProvider } from '../router' import { ETH_NATIVE_TOKEN_ADDRESS, isNativeToken, + isSameAddress, logExecutionTime, } from '../utils' import { @@ -50,6 +56,11 @@ export class RelayModuleSet extends SynapseModuleSet { } this.modules[chainId] = new RelayModule(chainId, address) }) + // HyperCore is a Relay destination, but has no EVM provider or contract. + this.modules[HYPERCORE_CHAIN_ID] = new RelayModule( + HYPERCORE_CHAIN_ID, + AddressZero + ) } public getModule(chainId: number): SynapseModule | undefined { @@ -71,17 +82,25 @@ export class RelayModuleSet extends SynapseModuleSet { toToken, fromToken, }: GetBridgeTokenCandidatesParameters): Promise { + const hyperCore = toChainId === HYPERCORE_CHAIN_ID // Check that both chains are supported - if (!this.getModule(fromChainId) || !this.getModule(toChainId)) { + if ( + !this.getModule(fromChainId) || + !this.getModule(toChainId) || + (hyperCore && toToken !== undefined && !this.isHyperCoreUsdc(toToken)) + ) { return [] } - // Relay supports origin swaps natively return [ { originChainId: fromChainId, destChainId: toChainId, originToken: fromToken, - destToken: toToken ?? ETH_NATIVE_TOKEN_ADDRESS, + destToken: + toToken ?? + (toChainId === HYPERCORE_CHAIN_ID + ? HYPERCORE_USDC_ADDRESS + : ETH_NATIVE_TOKEN_ADDRESS), }, ] } @@ -91,7 +110,12 @@ export class RelayModuleSet extends SynapseModuleSet { params: GetBridgeRouteV2Parameters ): Promise { const tokenZap = TOKEN_ZAP_V1_ADDRESS_MAP[params.bridgeToken.originChainId] - if (!this.validateBridgeRouteV2Params(params) || !tokenZap) { + if ( + !this.validateBridgeRouteV2Params(params) || + !tokenZap || + (params.bridgeToken.destChainId === HYPERCORE_CHAIN_ID && + !this.isHyperCoreUsdc(params.toToken)) + ) { return undefined } const quoteRequest: QuoteRequest = { @@ -99,7 +123,10 @@ export class RelayModuleSet extends SynapseModuleSet { originChainId: params.bridgeToken.originChainId, destinationChainId: params.bridgeToken.destChainId, originCurrency: addressToCurrency(params.originSwapRoute.toToken), - destinationCurrency: addressToCurrency(params.toToken), + destinationCurrency: + params.bridgeToken.destChainId === HYPERCORE_CHAIN_ID + ? HYPERCORE_USDC_ADDRESS + : addressToCurrency(params.toToken), amount: params.originSwapRoute.expectedToAmount.toString(), tradeType: TradeType.ExactInput, recipient: params.toRecipient ?? USER_SIMULATED_ADDRESS, @@ -201,4 +228,8 @@ export class RelayModuleSet extends SynapseModuleSet { payload: relayData.data, }) } + + private isHyperCoreUsdc(token: string): boolean { + return isSameAddress(token, HYPERCORE_USDC_ADDRESS) + } } diff --git a/packages/sdk-router/src/utils/TokenMetadataFetcher.ts b/packages/sdk-router/src/utils/TokenMetadataFetcher.ts index f049ff3ebd..5b66678365 100644 --- a/packages/sdk-router/src/utils/TokenMetadataFetcher.ts +++ b/packages/sdk-router/src/utils/TokenMetadataFetcher.ts @@ -2,7 +2,11 @@ import { Provider } from '@ethersproject/abstract-provider' import { Contract } from '@ethersproject/contracts' import { isNativeToken } from './addressUtils' -import { HYPERCORE_CHAIN_ID, SYN_ADDRESS_MAP } from '../constants' +import { + HYPERCORE_CHAIN_ID, + HYPERCORE_USDC_ADDRESS, + SYN_ADDRESS_MAP, +} from '../constants' import { logger } from './logger' import erc20ABI from '../abi/IERC20Metadata.json' import { marshallChainToken } from '../rfq/ticker' @@ -34,6 +38,13 @@ export class TokenMetadataFetcher { chainId: number, token: string ): Promise { + // Relay represents HyperCore's native USDC balance in 8-decimal units. + if ( + chainId === HYPERCORE_CHAIN_ID && + token.toLowerCase() === HYPERCORE_USDC_ADDRESS + ) { + return 8 + } // HyperCore SYN uses its native token identifier and 8-decimal balance units. if ( chainId === HYPERCORE_CHAIN_ID && diff --git a/packages/synapse-interface/README.md b/packages/synapse-interface/README.md index 8780db82a4..db93c2ca3f 100644 --- a/packages/synapse-interface/README.md +++ b/packages/synapse-interface/README.md @@ -138,4 +138,4 @@ the Goldsky secret; without its QuickNode binding, the proxy returns HTTP 500. ### Routes -SYN uses the existing bridge UI for Ethereum ↔ HyperEVM and Ethereum → HyperCore. Select **Hyperliquid** for HyperCore (non-EVM routing identifier `1337`); HyperEVM is `999`. HyperCore SYN uses its native 16-byte token ID and 8-decimal amounts. HyperCore recipients must already be activated. SYN uses the composer in one source transaction, while USDC retains its Arbitrum deposit flow. Module pauses can target `SYN`, including destination `1337`. See the [SDK integration notes](../sdk-router/README.md#syn-bridging) for quote units, fees, and tracking. +SYN uses the existing bridge UI for Ethereum ↔ HyperEVM and Ethereum → HyperCore. Select **Hyperliquid** for HyperCore (non-EVM routing identifier `1337`); HyperEVM is `999`. HyperCore SYN uses its native 16-byte token ID and 8-decimal amounts. HyperCore recipients must already be activated. SYN uses the composer in one source transaction, while USDC uses Relay directly to Hyperliquid Perps. Module pauses can target `SYN`, including destination `1337`. See the [SDK integration notes](../sdk-router/README.md#syn-bridging) for quote units, fees, and tracking. diff --git a/packages/synapse-interface/__tests__/getPendingBridgeTransactionTrackingData.test.ts b/packages/synapse-interface/__tests__/getPendingBridgeTransactionTrackingData.test.ts index d5b3387cd9..39482c37d9 100644 --- a/packages/synapse-interface/__tests__/getPendingBridgeTransactionTrackingData.test.ts +++ b/packages/synapse-interface/__tests__/getPendingBridgeTransactionTrackingData.test.ts @@ -91,28 +91,20 @@ describe('getPendingBridgeTransactionTrackingData', () => { }) }) - it('keeps saved Hyperliquid deposits working without an explicit tracked timestamp', () => { - const trackedTransaction = getPendingBridgeTransactionTrackingData( - { - id: 333, - originChain: mockChain, - originToken: mockToken, - originValue: '5', - destinationChain: { ...HYPERLIQUID, id: 998 }, - transactionHash: '0xhyper', - isSubmitted: true, - }, - zeroAddress - ) - - expect(trackedTransaction).toMatchObject({ - originTxHash: '0xhyper', - destinationChain: HYPERLIQUID, - estimatedTime: 0, - timestamp: 333, - bridgeModuleName: '', - routerAddress: zeroAddress, - destinationToken: mockToken, - }) + it('does not promote legacy Hyperliquid deposits without a tracked timestamp', () => { + expect( + getPendingBridgeTransactionTrackingData( + { + id: 333, + originChain: mockChain, + originToken: mockToken, + originValue: '5', + destinationChain: { ...HYPERLIQUID, id: 998 }, + transactionHash: '0xhyper', + isSubmitted: true, + }, + zeroAddress + ) + ).toBeNull() }) }) diff --git a/packages/synapse-interface/__tests__/persistedTransactionsMigration.test.ts b/packages/synapse-interface/__tests__/persistedTransactionsMigration.test.ts new file mode 100644 index 0000000000..dcb09792d7 --- /dev/null +++ b/packages/synapse-interface/__tests__/persistedTransactionsMigration.test.ts @@ -0,0 +1,61 @@ +import { migratePersistedState } from '@/store/migrations' + +describe('persisted transaction migration', () => { + it('removes only old Arbitrum USDC deposits to HyperCore', async () => { + const legacyPending = { + originChain: { id: 42161 }, + originToken: { routeSymbol: 'USDC' }, + destinationChain: { id: 998, chainSymbol: 'HYPERLIQUID' }, + } + const legacyActivity = { + ...legacyPending, + destinationToken: { routeSymbol: 'USDC' }, + bridgeModuleName: '', + status: 'pending', + } + const relayDeposit = { ...legacyActivity, bridgeModuleName: 'Relay' } + const synDeposit = { + ...legacyActivity, + originToken: { routeSymbol: 'SYN' }, + destinationToken: { routeSymbol: 'SYN' }, + bridgeModuleName: 'SYN', + } + const unrelated = { + ...legacyActivity, + destinationChain: { id: 1, chainSymbol: 'ETHEREUM' }, + } + const state = { + _persist: { version: 3, rehydrated: false }, + application: { lastConnectedAddress: '0xabc' }, + transactions: { + pendingBridgeTransactions: [legacyPending, relayDeposit, unrelated], + userHistoricalTransactions: ['history'], + }, + _transactions: { + transactions: [legacyActivity, relayDeposit, synDeposit, unrelated], + }, + } + + const migrated = (await migratePersistedState(state, 4)) as typeof state + + expect(migrated.transactions.pendingBridgeTransactions).toEqual([ + relayDeposit, + unrelated, + ]) + expect(migrated._transactions.transactions).toEqual([ + relayDeposit, + synDeposit, + unrelated, + ]) + expect(migrated.application).toEqual(state.application) + expect(migrated.transactions.userHistoricalTransactions).toEqual([ + 'history', + ]) + expect( + await migratePersistedState( + { ...state, _persist: { version: 4, rehydrated: true } }, + 4 + ) + ).toEqual({ ...state, _persist: { version: 4, rehydrated: true } }) + }) +}) diff --git a/packages/synapse-interface/__tests__/synBridgeRoutes.test.ts b/packages/synapse-interface/__tests__/synBridgeRoutes.test.ts index f9bbb5ab77..14a748ecfc 100644 --- a/packages/synapse-interface/__tests__/synBridgeRoutes.test.ts +++ b/packages/synapse-interface/__tests__/synBridgeRoutes.test.ts @@ -1,7 +1,10 @@ -import { HYPERCORE_CHAIN_ID } from '@synapsecns/sdk-router' +import { + HYPERCORE_CHAIN_ID, + RELAY_SUPPORTED_CHAIN_IDS, +} from '@synapsecns/sdk-router' import { EXISTING_BRIDGE_ROUTES } from '@/constants/existingBridgeRoutes' -import { SYN } from '@/constants/tokens/bridgeable' +import { SYN, USDC } from '@/constants/tokens/bridgeable' import { Token } from '@/utils/types' import { HYPERLIQUID } from '@/constants/chains/master' import { getFromChainIds } from '@/utils/routeMaker/getFromChainIds' @@ -9,10 +12,6 @@ import { getToChainIds } from '@/utils/routeMaker/getToChainIds' import { getToTokens } from '@/utils/routeMaker/getToTokens' import { getPausedBridgeModuleNamesForRoute } from '@/utils/getPausedBridgeModuleNamesForRoute' import { isValidBridgeModule } from '@/components/Maintenance/functions/isValidBridgeModule' -import { - getBridgeDestinationChainId, - isHyperliquidUsdcDeposit, -} from '@/utils/hyperliquid' describe('SYN routes in the existing bridge selectors', () => { it('supports module and ALL pauses on the virtual destination', () => { @@ -53,8 +52,6 @@ describe('SYN routes in the existing bridge selectors', () => { ).toContain(`SYN-${toChainId}`) expect(SYN.addresses[toChainId]).toBeDefined() expect(SYN.decimals[toChainId]).toBe(toChainId === 1337 ? 8 : 18) - expect(getBridgeDestinationChainId(toChainId, SYN)).toBe(toChainId) - expect(isHyperliquidUsdcDeposit(toChainId, SYN)).toBe(false) } ) @@ -86,7 +83,7 @@ describe('SYN routes in the existing bridge selectors', () => { }) ).toEqual([1]) expect(EXISTING_BRIDGE_ROUTES['SYN-1337']).toBeUndefined() - expect(EXISTING_BRIDGE_ROUTES['SYN-999']).toEqual(['SYN-1']) + expect(EXISTING_BRIDGE_ROUTES['SYN-999']).toContain('SYN-1') }) it('keeps HyperCore in the destination selector after all fields are selected', () => { @@ -112,11 +109,25 @@ describe('SYN routes in the existing bridge selectors', () => { expect(EXISTING_BRIDGE_ROUTES['SYN-1']).toContain('SYN-42161') }) - it('preserves the USDC deposit routing through Arbitrum', () => { - expect(getBridgeDestinationChainId(1337, { routeSymbol: 'USDC' })).toBe( - 42161 - ) - expect(isHyperliquidUsdcDeposit(1337, { routeSymbol: 'USDC' })).toBe(true) - expect(EXISTING_BRIDGE_ROUTES['USDC-42161']).toContain('USDC-1337') + it('offers Hyperliquid USDC from every Relay origin token', () => { + expect(USDC.addresses[1337]).toBe('0x00000000000000000000000000000000') + expect(USDC.decimals[1337]).toBe(8) + Object.entries(EXISTING_BRIDGE_ROUTES).forEach(([origin, destinations]) => { + const originChainId = Number(origin.split('-').at(-1)) + expect(destinations.includes('USDC-1337')).toBe( + RELAY_SUPPORTED_CHAIN_IDS.includes(originChainId) + ) + }) + expect(EXISTING_BRIDGE_ROUTES['DAI-8453']).toContain('USDC-1337') + expect(EXISTING_BRIDGE_ROUTES['HYPE-999']).toContain('USDC-1337') + expect( + getToTokens({ + fromChainId: 999, + fromTokenRouteSymbol: 'HYPE', + toChainId: 1337, + toTokenRouteSymbol: null, + }) + ).toContain('USDC-1337') + expect(EXISTING_BRIDGE_ROUTES['USDC-25'] ?? []).not.toContain('USDC-1337') }) }) diff --git a/packages/synapse-interface/__tests__/synRecipientWarning.test.tsx b/packages/synapse-interface/__tests__/synRecipientWarning.test.tsx index 6afaf358de..1323c5c8be 100644 --- a/packages/synapse-interface/__tests__/synRecipientWarning.test.tsx +++ b/packages/synapse-interface/__tests__/synRecipientWarning.test.tsx @@ -72,9 +72,6 @@ jest.mock( }), }) ) -jest.mock('../utils/hyperliquid', () => ({ - isHyperliquidUsdcDeposit: () => false, -})) jest.mock('../contexts/SegmentAnalyticsProvider', () => ({ segmentAnalyticsEvent: jest.fn(), })) diff --git a/packages/synapse-interface/__tests__/synTransactionStatus.test.tsx b/packages/synapse-interface/__tests__/synTransactionStatus.test.tsx index d4ac9e9024..351afbb94a 100644 --- a/packages/synapse-interface/__tests__/synTransactionStatus.test.tsx +++ b/packages/synapse-interface/__tests__/synTransactionStatus.test.tsx @@ -27,8 +27,7 @@ describe('HyperCore transaction display', () => { it.each([ [SYN, 'SYN', 'pending', 'pending', 1337], [SYN, 'SYN', 'completed', 'completed', 1337], - [USDC, '', 'pending', 'completed', 1337], - [USDC, '', 'pending', 'completed', 998], + [USDC, 'Relay', 'pending', 'pending', 1337], ])( 'uses the tracked status for %s through %s (%s)', (token, bridgeModuleName, storedStatus, displayedStatus, storedChainId) => { diff --git a/packages/synapse-interface/components/HyperliquidDepositInfo.tsx b/packages/synapse-interface/components/HyperliquidDepositInfo.tsx deleted file mode 100644 index 6d04bd8e1d..0000000000 --- a/packages/synapse-interface/components/HyperliquidDepositInfo.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { ARBITRUM } from '@/constants/chains/master' - -export const HyperliquidDepositInfo = ({ - fromChainId, - isOnArbitrum, - hasDepositedOnHyperliquid, -}) => { - if (fromChainId !== ARBITRUM.id) { - return ( -
-
-
-
Step 1
-
- -
Bridge (Arbitrum)
-
-
-
-
Step 2
-
- -
Deposit (Hyperliquid)
-
-
-
-
- ) - } - - if (hasDepositedOnHyperliquid) { - return ( -
-
-
-
Step 1
-
- -
Bridge (Arbitrum)
-
-
-
-
Step 2
-
- -
Deposit (Hyperliquid)
-
-
-
-
- ) - } - - if (fromChainId === ARBITRUM.id && isOnArbitrum) { - return ( -
-
-
-
Step 1
-
- -
Bridge (Arbitrum)
-
-
-
-
Step 2
-
- -
Deposit (Hyperliquid)
-
-
-
-
- ) - } -} - -const CompletedCheckMarkCircle = () => { - return ( - - - - - - ) -} - -const GreenStep1Circle = () => { - return ( - - - - - ) -} - -const GreenStep2Circle = () => { - return ( - - - - - ) -} - -const GrayStep2Circle = () => { - return ( - - - - - ) -} diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx index 203427fec5..b0d4e1f2c6 100644 --- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx @@ -13,8 +13,6 @@ import { TransactionButton } from '@/components/buttons/TransactionButton' import { useBridgeValidations } from './hooks/useBridgeValidations' import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' import { useConfirmNewBridgePrice } from './hooks/useConfirmNewBridgePrice' -import { isHyperliquidUsdcDeposit } from '@/utils/hyperliquid' -import { HYPERLIQUID_MINIMUM_DEPOSIT } from '@/constants' export const BridgeTransactionButton = ({ approveTxn, @@ -53,16 +51,16 @@ export const BridgeTransactionButton = ({ debouncedFromValue, } = useBridgeState() const { bridgeQuote, isLoading } = useBridgeQuoteState() - const { isPendingConfirmChange, onUserAcceptChange } = - useConfirmNewBridgePrice() + const { + isPendingConfirmChange, + onUserAcceptChange, + } = useConfirmNewBridgePrice() const { isWalletPending } = useWalletState() - const { showDestinationWarning, isDestinationWarningAccepted } = - useBridgeDisplayState() - - const isUsdcDeposit = isHyperliquidUsdcDeposit(toChainId, toToken) - const hasHyperliquidMinDeposit = - !isUsdcDeposit || Number(debouncedFromValue) > HYPERLIQUID_MINIMUM_DEPOSIT + const { + showDestinationWarning, + isDestinationWarningAccepted, + } = useBridgeDisplayState() const { hasValidInput, @@ -83,8 +81,7 @@ export const BridgeTransactionButton = ({ (isConnected && !hasValidQuote) || (isConnected && !hasSufficientBalance) || (isConnected && isQuoteStale) || - (!isUsdcDeposit && destinationAddress && !isAddress(destinationAddress)) || - !hasHyperliquidMinDeposit + (destinationAddress && !isAddress(destinationAddress)) let buttonProperties @@ -139,11 +136,6 @@ export const BridgeTransactionButton = ({ label: t('Connect Wallet to Bridge'), onClick: openConnectModal, } - } else if (!hasHyperliquidMinDeposit) { - buttonProperties = { - label: `${HYPERLIQUID_MINIMUM_DEPOSIT} USDC Minimum`, - onClick: null, - } } else if ( bridgeQuote.bridgeModuleName !== null && !isLoading && @@ -166,19 +158,11 @@ export const BridgeTransactionButton = ({ destinationTokenAddressForState: toToken.addresses[toChainId], bridgeQuote, }) - } else if ( - !isUsdcDeposit && - destinationAddress && - !isAddress(destinationAddress) - ) { + } else if (destinationAddress && !isAddress(destinationAddress)) { buttonProperties = { label: t('Invalid Destination address'), } - } else if ( - !isUsdcDeposit && - showDestinationWarning && - !isDestinationWarningAccepted - ) { + } else if (showDestinationWarning && !isDestinationWarningAccepted) { buttonProperties = { label: t('Confirm destination address'), onClick: () => dispatch(setIsDestinationWarningAccepted(true)), diff --git a/packages/synapse-interface/components/StateManagedBridge/HyperliquidDepositButton.tsx b/packages/synapse-interface/components/StateManagedBridge/HyperliquidDepositButton.tsx deleted file mode 100644 index f089a15ca2..0000000000 --- a/packages/synapse-interface/components/StateManagedBridge/HyperliquidDepositButton.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { useEffect, useState } from 'react' -import { useAccount, useAccountEffect, useSwitchChain } from 'wagmi' -import { useConnectModal } from '@rainbow-me/rainbowkit' -import { useTranslations } from 'next-intl' -import { erc20Abi } from 'viem' -import { - simulateContract, - waitForTransactionReceipt, - writeContract, -} from '@wagmi/core' - -import { wagmiConfig } from '@/wagmiConfig' -import { useAppDispatch } from '@/store/hooks' -import { useWalletState } from '@/slices/wallet/hooks' -import { useBridgeState } from '@/slices/bridge/hooks' -import { TransactionButton } from '@/components/buttons/TransactionButton' -import { useBridgeValidations } from './hooks/useBridgeValidations' -import { USDC } from '@/constants/tokens/bridgeable' -import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master' -import { stringToBigInt } from '@/utils/bigint/format' -import { fetchAndStoreSingleNetworkPortfolioBalances } from '@/slices/portfolio/hooks' -import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' -import { addPendingBridgeTransaction } from '@/slices/transactions/actions' -import { getUnixTimeMinutesFromNow } from '@/utils/time' -import { HYPERLIQUID_MINIMUM_DEPOSIT } from '@/constants' - -const HYPERLIQUID_DEPOSIT_ADDRESS = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7' - -const deposit = async (amount: bigint) => { - try { - const { request } = await simulateContract(wagmiConfig, { - chainId: ARBITRUM.id, - address: USDC.addresses[ARBITRUM.id], - abi: erc20Abi, - functionName: 'transfer', - args: [HYPERLIQUID_DEPOSIT_ADDRESS, amount], - }) - - const hash = await writeContract(wagmiConfig, request) - - const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash }) - - return txReceipt - } catch (error) { - console.error('Confirmation error:', error) - throw error - } -} - -export const HyperliquidTransactionButton = ({ - isTyping, - hasDepositedOnHyperliquid, - setHasDepositedOnHyperliquid, -}) => { - const [isDepositing, setIsDepositing] = useState(false) - - const { address } = useAccount() - - const dispatch = useAppDispatch() - const { openConnectModal } = useConnectModal() - const [isConnected, setIsConnected] = useState(false) - - const { isConnected: isConnectedInit } = useAccount() - const { chains, switchChain } = useSwitchChain() - - const { fromToken, fromChainId, debouncedFromValue } = useBridgeState() - - const { isWalletPending } = useWalletState() - - const { hasValidInput, hasSufficientBalance, onSelectedChain } = - useBridgeValidations() - - const depositingMinimumAmount = - Number(debouncedFromValue) >= HYPERLIQUID_MINIMUM_DEPOSIT - - const t = useTranslations('Bridge') - - const amount = stringToBigInt( - debouncedFromValue, - fromToken?.decimals[fromChainId] - ) - - const handleDeposit = async () => { - setIsDepositing(true) - const currentTimestamp: number = getUnixTimeMinutesFromNow(0) - try { - const txReceipt = await deposit(amount) - - setHasDepositedOnHyperliquid(true) - segmentAnalyticsEvent(`[Hyperliquid Deposit]`, { - inputAmount: debouncedFromValue, - }) - dispatch( - fetchAndStoreSingleNetworkPortfolioBalances({ - address, - chainId: ARBITRUM.id, - }) - ) - dispatch( - addPendingBridgeTransaction({ - id: currentTimestamp, - originChain: ARBITRUM, - originToken: fromToken, - originValue: debouncedFromValue, - destinationChain: HYPERLIQUID, - destinationToken: undefined, - transactionHash: txReceipt.transactionHash, - timestamp: undefined, - isSubmitted: false, - estimatedTime: undefined, - bridgeModuleName: undefined, - destinationAddress: undefined, - routerAddress: undefined, - }) - ) - } catch (error) { - console.error('Deposit error:', error) - } finally { - setIsDepositing(false) - } - } - - useAccountEffect({ - onDisconnect() { - setIsConnected(false) - }, - }) - - useEffect(() => { - setIsConnected(isConnectedInit) - }, [isConnectedInit]) - - const isButtonDisabled = - isTyping || - isDepositing || - !depositingMinimumAmount || - isWalletPending || - !hasValidInput || - (isConnected && !hasSufficientBalance) - - let buttonProperties - - if (isConnected && !hasSufficientBalance) { - buttonProperties = { - label: t('Insufficient balance'), - onClick: null, - } - } else if (!depositingMinimumAmount) { - buttonProperties = { - label: `${HYPERLIQUID_MINIMUM_DEPOSIT} USDC Minimum`, - onClick: null, - } - } else if (!isConnected && hasValidInput) { - buttonProperties = { - label: t('Connect Wallet to Bridge'), - onClick: openConnectModal, - } - } else if (!onSelectedChain && hasValidInput) { - buttonProperties = { - label: t('Switch to {chainName}', { - chainName: chains.find((c) => c.id === fromChainId)?.name, - }), - onClick: () => switchChain({ chainId: fromChainId }), - pendingLabel: t('Switching chains'), - } - } else { - buttonProperties = { - onClick: handleDeposit, - label: t('Deposit {symbol}', { symbol: fromToken?.symbol }), - pendingLabel: t('Depositing'), - } - } - - return ( - buttonProperties && ( - <> -
- -
- - ) - ) -} diff --git a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx index 7cca5ca61a..9db3b450be 100644 --- a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx @@ -16,8 +16,6 @@ import { useWalletState } from '@/slices/wallet/hooks' import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks' import { useBridgeValidations } from './hooks/useBridgeValidations' import { useTranslations } from 'next-intl' -import { ARBITRUM } from '@/constants/chains/master' -import { isHyperliquidUsdcDeposit } from '@/utils/hyperliquid' import { useUsdDisplay } from '@hooks/useUsdDisplay' import { formatInlineUsdDifference } from '@utils/calculateUsdValue' import { usePortfolioBalances } from '@/slices/portfolio/hooks' @@ -36,10 +34,7 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => { const { bridgeQuote, isLoading } = useBridgeQuoteState() const { showDestinationAddress } = useBridgeDisplayState() const { hasValidInput, hasValidQuote } = useBridgeValidations() - const { debouncedFromValue, fromChainId, toChainId, toToken } = - useBridgeState() - const isUsdcDeposit = isHyperliquidUsdcDeposit(toChainId, toToken) - const isDirectUsdcDeposit = fromChainId === ARBITRUM.id && isUsdcDeposit + const { fromChainId, toChainId, toToken } = useBridgeState() const showValue = useMemo(() => { if (!hasValidInput) { @@ -54,8 +49,7 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => { const inputClassName = isQuoteStale ? 'opacity-50' : undefined // Fetch token price and calculate USD value - const outputValue = isDirectUsdcDeposit ? debouncedFromValue : showValue - const usdValue = useUsdDisplay(toToken, outputValue) + const usdValue = useUsdDisplay(toToken, showValue) // Convert input amount to bigint for slippage calculation const inputAmount = parseTokenAmount( @@ -87,13 +81,15 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => { ? getParsedBalance(toTokenBalance, toTokenDecimals) : '0.0' const formattedBalance = formatAmount(parsedBalance) - const formattedUsdValue = `${usdValue}${formatInlineUsdDifference(usdDifference)}` + const formattedUsdValue = `${usdValue}${formatInlineUsdDifference( + usdDifference + )}` return (
- {showDestinationAddress && !isUsdcDeposit ? ( + {showDestinationAddress ? ( ) : null}
@@ -103,7 +99,7 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts index 052d10e6c5..9182ee84ca 100644 --- a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts +++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts @@ -8,7 +8,6 @@ import { BridgeQuoteState } from '@/slices/bridgeQuote/reducer' import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge' import { hasOnlyZeroes } from '@/utils/hasOnlyZeroes' import { useBridgeSelections } from './useBridgeSelections' -import { getBridgeDestinationChainId } from '@/utils/hyperliquid' export const useBridgeValidations = () => { const { chainId } = useAccount() @@ -67,7 +66,7 @@ export const useBridgeValidations = () => { debouncedFromValue, fromChainId, fromToken, - getBridgeDestinationChainId(toChainId, toToken), + toChainId, toToken ) }, [debouncedFromValue, fromChainId, fromToken, toChainId, toToken]) diff --git a/packages/synapse-interface/components/_Transaction/_Transactions.tsx b/packages/synapse-interface/components/_Transaction/_Transactions.tsx index 97bdc8151b..8199e43e37 100644 --- a/packages/synapse-interface/components/_Transaction/_Transactions.tsx +++ b/packages/synapse-interface/components/_Transaction/_Transactions.tsx @@ -8,7 +8,6 @@ import { ALL_TOKENS } from '@/constants/tokens/master' import { CHAINS_BY_ID } from '@/constants/chains' import { useWalletState } from '@/slices/wallet/hooks' import { HYPERLIQUID } from '@/constants/chains/master' -import { isHyperliquidUsdcDeposit } from '@/utils/hyperliquid' /** TODO: Update naming once refactoring of previous Activity/Tx flow is done */ export const _Transactions = ({ @@ -66,15 +65,7 @@ export const _Transactions = ({ kappa={tx?.kappa} timestamp={tx.timestamp} currentTime={currentTime} - status={ - !tx.bridgeModuleName && - isHyperliquidUsdcDeposit( - destinationChain?.id, - destinationToken - ) - ? 'completed' - : tx.status - } + status={tx.status} disabled={isWalletPending} /> ) diff --git a/packages/synapse-interface/constants/bridgeMap.ts b/packages/synapse-interface/constants/bridgeMap.ts index cae1f8de7f..4acf8a3bf7 100644 --- a/packages/synapse-interface/constants/bridgeMap.ts +++ b/packages/synapse-interface/constants/bridgeMap.ts @@ -926,7 +926,7 @@ export const BRIDGE_MAP = { }, '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1': { decimals: 6, - symbol: 'USDC.e', + symbol: 'USDC', origin: ['RFQ.USDC'], destination: ['RFQ.USDC'], swappable: [], diff --git a/packages/synapse-interface/constants/existingBridgeRoutes.ts b/packages/synapse-interface/constants/existingBridgeRoutes.ts index 74bd523758..1c385302d6 100644 --- a/packages/synapse-interface/constants/existingBridgeRoutes.ts +++ b/packages/synapse-interface/constants/existingBridgeRoutes.ts @@ -1,4 +1,5 @@ import _ from 'lodash' +import { RELAY_SUPPORTED_CHAIN_IDS } from '@synapsecns/sdk-router' import { BRIDGE_MAP } from '@/constants/bridgeMap' import { flattenPausedTokens } from '@/utils/flattenPausedTokens' @@ -49,22 +50,20 @@ const constructJSON = (swappableMap, exclusionList) => { return result } -const addUSDCHyperLiquid = (routes) => { +const addUSDCHyperLiquid = (routes: BridgeRoutes): BridgeRoutes => { const usdcHyperliquid = `USDC-${HYPERLIQUID.id}` return _.mapValues(routes, (innerList, key) => { - // If the key is USDC-42161 OR if the innerList includes USDC-42161 - if (key === 'USDC-42161' || innerList.includes('USDC-42161')) { - return [...innerList, usdcHyperliquid] + const originChainId = Number(key.slice(key.lastIndexOf('-') + 1)) + if (RELAY_SUPPORTED_CHAIN_IDS.includes(originChainId)) { + return [...new Set([...innerList, usdcHyperliquid])] } return innerList }) } const PAUSED_TOKENS = flattenPausedTokens() -export const EXISTING_BRIDGE_ROUTES: BridgeRoutes = addUSDCHyperLiquid( - constructJSON(BRIDGE_MAP, PAUSED_TOKENS) -) +const bridgeRoutes: BridgeRoutes = constructJSON(BRIDGE_MAP, PAUSED_TOKENS) // Separate from the legacy SYN graph, which would advertise every SYN chain. const synRoutes: BridgeRoutes = { @@ -73,12 +72,15 @@ const synRoutes: BridgeRoutes = { } Object.entries(synRoutes).forEach(([origin, destinations]) => { if (PAUSED_TOKENS.includes(origin)) return - EXISTING_BRIDGE_ROUTES[origin] = [ + bridgeRoutes[origin] = [ ...new Set([ - ...(EXISTING_BRIDGE_ROUTES[origin] ?? []), + ...(bridgeRoutes[origin] ?? []), ...destinations.filter( (destination) => !PAUSED_TOKENS.includes(destination) ), ]), ] }) + +export const EXISTING_BRIDGE_ROUTES: BridgeRoutes = + addUSDCHyperLiquid(bridgeRoutes) diff --git a/packages/synapse-interface/constants/index.ts b/packages/synapse-interface/constants/index.ts index bcbf8a4ed2..85e1dee41c 100644 --- a/packages/synapse-interface/constants/index.ts +++ b/packages/synapse-interface/constants/index.ts @@ -2,5 +2,3 @@ export const MAX_UINT256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935n export const ETHEREUM_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' - -export const HYPERLIQUID_MINIMUM_DEPOSIT = 5 diff --git a/packages/synapse-interface/constants/tokens/bridgeable.ts b/packages/synapse-interface/constants/tokens/bridgeable.ts index 9dbe4f3755..f45fcbab91 100644 --- a/packages/synapse-interface/constants/tokens/bridgeable.ts +++ b/packages/synapse-interface/constants/tokens/bridgeable.ts @@ -422,6 +422,8 @@ export const USDC = new Token({ [CHAINS.SCROLL.id]: '0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4', [CHAINS.LINEA.id]: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', [CHAINS.UNICHAIN.id]: '0x078d782b760474a361dda0af3839290b0ef57ad6', + [CHAINS.WORLDCHAIN.id]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1', + [CHAINS.HYPERLIQUID.id]: '0x00000000000000000000000000000000', }, decimals: { [CHAINS.ETH.id]: 6, @@ -440,6 +442,8 @@ export const USDC = new Token({ [CHAINS.SCROLL.id]: 6, [CHAINS.LINEA.id]: 6, [CHAINS.UNICHAIN.id]: 6, + [CHAINS.WORLDCHAIN.id]: 6, + [CHAINS.HYPERLIQUID.id]: 8, }, symbol: 'USDC', name: 'USD Coin', @@ -902,7 +906,6 @@ export const USDCe = new Token({ [CHAINS.ARBITRUM.id]: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', [CHAINS.AURORA.id]: '0xB12BFcA5A55806AaF64E99521918A4bf0fC40802', [CHAINS.POLYGON.id]: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', - [CHAINS.WORLDCHAIN.id]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1', [CHAINS.BERACHAIN.id]: '0x549943e04f40284185054145c6E4e9568C1D3241', }, decimals: 6, diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index 1176137d39..d20f7fd9e1 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -9,7 +9,6 @@ import { getWalletClient, getPublicClient, waitForTransactionReceipt, - switchChain, } from '@wagmi/core' import { useTranslations } from 'next-intl' @@ -76,20 +75,11 @@ import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejecte import { BridgeQuoteResetTimer } from '@/components/StateManagedBridge/BridgeQuoteResetTimer' import { useBridgeValidations } from '@/components/StateManagedBridge/hooks/useBridgeValidations' import { useStaleQuoteUpdater } from '@/components/StateManagedBridge/hooks/useStaleQuoteUpdater' -import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master' -import { HyperliquidTransactionButton } from '@/components/StateManagedBridge/HyperliquidDepositButton' -import { USDC } from '@/constants/tokens/bridgeable' -import { CheckCircleIcon } from '@heroicons/react/outline' -import Image from 'next/image' -import { HyperliquidDepositInfo } from '@/components/HyperliquidDepositInfo' -import { - getBridgeDestinationChainId, - isHyperliquidUsdcDeposit, -} from '@/utils/hyperliquid' +import { HYPERLIQUID } from '@/constants/chains/master' const StateManagedBridge = () => { const dispatch = useAppDispatch() - const { address, isConnected, chain: connectedChain } = useAccount() + const { address, isConnected } = useAccount() const { balances } = usePortfolioState() const { synapseSDK } = useSynapseContext() const router = useRouter() @@ -104,9 +94,6 @@ const StateManagedBridge = () => { const [isTyping, setIsTyping] = useState(false) - const [hasDepositedOnHyperliquid, setHasDepositedOnHyperliquid] = - useState(false) - const { fromChainId, toChainId, @@ -116,22 +103,11 @@ const StateManagedBridge = () => { destinationAddress, }: BridgeState = useBridgeState() - const isUsdcDeposit = isHyperliquidUsdcDeposit(toChainId, toToken) - const isDirectUsdcDeposit = fromChainId === ARBITRUM.id && isUsdcDeposit - const bridgeDestinationAddress = isUsdcDeposit - ? undefined - : destinationAddress - const bridgeDestinationChainId = getBridgeDestinationChainId( - toChainId, - toToken - ) - const { bridgeQuote, isLoading } = useBridgeQuoteState() const [acceptedSynRecipient, setAcceptedSynRecipient] = useState(null) - const synRecipient = ( - destinationAddress && isAddress(destinationAddress) - ? destinationAddress - : address + const synRecipient = (destinationAddress && isAddress(destinationAddress) + ? destinationAddress + : address )?.toLowerCase() const isSynHyperCore = toChainId === HYPERLIQUID.id && @@ -168,10 +144,9 @@ const StateManagedBridge = () => { BridgeMaintenanceProgressBar, BridgeMaintenanceWarningMessage, } = useMaintenance() - const pausedModulesKey = useMemo( - () => JSON.stringify(pausedModulesList), - [pausedModulesList] - ) + const pausedModulesKey = useMemo(() => JSON.stringify(pausedModulesList), [ + pausedModulesList, + ]) useEffect(() => { segmentAnalyticsEvent( @@ -223,14 +198,14 @@ const StateManagedBridge = () => { fetchBridgeQuote({ synapseSDK, fromChainId, - toChainId: bridgeDestinationChainId, + toChainId, fromToken, toToken, debouncedFromValue, requestId: thisRequestId, currentTimestamp, address, - destinationAddress: bridgeDestinationAddress, + destinationAddress, pausedModulesList, }) ) @@ -253,7 +228,7 @@ const StateManagedBridge = () => { quoteToastRef.current.id = toast(message, { duration: 3000 }) } - if (fetchBridgeQuote.rejected.match(result) && !isDirectUsdcDeposit) { + if (fetchBridgeQuote.rejected.match(result)) { const message = t( 'No route found for bridging {debouncedFromValue} {fromToken} on {fromChainId} to {toToken} on {toChainId}', { @@ -335,8 +310,8 @@ const StateManagedBridge = () => { const pendingTransactionId: number = getUnixTimeSecondsNow() - if (bridgeDestinationAddress) { - const isRisky = await screenAddress(bridgeDestinationAddress) + if (destinationAddress) { + const isRisky = await screenAddress(destinationAddress) if (isRisky) { return } @@ -347,7 +322,7 @@ const StateManagedBridge = () => { { id: bridgeQuote.id, originChainId: fromChainId, - destinationChainId: bridgeDestinationChainId, + destinationChainId: toChainId, inputAmount: debouncedFromValue, expectedReceivedAmount: bridgeQuote.outputAmountString, slippage: bridgeQuote.exchangeRate, @@ -366,14 +341,14 @@ const StateManagedBridge = () => { originChain: CHAINS_BY_ID[fromChainId], originToken: fromToken, originValue: debouncedFromValue, - destinationChain: CHAINS_BY_ID[bridgeDestinationChainId], + destinationChain: CHAINS_BY_ID[toChainId], destinationToken: toToken, transactionHash: undefined, timestamp: undefined, isSubmitted: false, estimatedTime: bridgeQuote.estimatedTime, bridgeModuleName: bridgeQuote.bridgeModuleName, - destinationAddress: bridgeDestinationAddress, + destinationAddress, routerAddress: bridgeQuote.routerAddress, }) ) @@ -417,7 +392,7 @@ const StateManagedBridge = () => { segmentAnalyticsEvent(`[Bridge] bridges successfully`, { id: bridgeQuote.id, originChainId: fromChainId, - destinationChainId: bridgeDestinationChainId, + destinationChainId: toChainId, inputAmount: debouncedFromValue, expectedReceivedAmount: bridgeQuote.outputAmountString, slippage: bridgeQuote.exchangeRate, @@ -472,13 +447,6 @@ const StateManagedBridge = () => { timeout: 60_000, }) - if (isUsdcDeposit) { - dispatch(setFromChainId(ARBITRUM.id)) - dispatch(setFromToken(USDC)) - dispatch(setToChainId(HYPERLIQUID.id)) - switchChain(wagmiConfig, { chainId: ARBITRUM.id }) - } - /** Update Origin Chain token balances after resolved tx or timeout reached */ /** Assume tx has been actually resolved if above times out */ dispatch( @@ -556,20 +524,13 @@ const StateManagedBridge = () => { /> - {!isDirectUsdcDeposit && } + - {isUsdcDeposit && ( - - )} - {!isUsdcDeposit && } + {showSynRecipientWarning && ( { /> )}
- {isDirectUsdcDeposit ? ( - - ) : ( - - )} +
+ tx.originChain?.id === 42161 && + tx.destinationChain?.chainSymbol === 'HYPERLIQUID' && + (tx.destinationToken ?? tx.originToken)?.routeSymbol === 'USDC' && + !tx.bridgeModuleName + +export const migratePersistedState = createMigrate({ + 4: (state) => { + if (!state) return state + const { transactions, _transactions } = state as typeof state & + Partial> + return { + ...state, + ...(transactions && { + transactions: { + ...transactions, + pendingBridgeTransactions: + transactions.pendingBridgeTransactions.filter( + (tx) => !isLegacyHyperliquidDeposit(tx) + ), + }, + }), + ...(_transactions && { + _transactions: { + ..._transactions, + transactions: _transactions.transactions.filter( + (tx) => !isLegacyHyperliquidDeposit(tx) + ), + }, + }), + } + }, +}) diff --git a/packages/synapse-interface/store/reducer.ts b/packages/synapse-interface/store/reducer.ts index f6d940f210..9dc8fb9287 100644 --- a/packages/synapse-interface/store/reducer.ts +++ b/packages/synapse-interface/store/reducer.ts @@ -20,6 +20,7 @@ import wallet from '@/slices/wallet/reducer' import bridgeQuote from '@/slices/bridgeQuote/reducer' import { api } from '@/slices/api/slice' import { RootActions } from '@/slices/application/actions' +import { migratePersistedState } from './migrations' const persistedReducers = { application, @@ -30,10 +31,11 @@ const persistedReducers = { export const storageKey: string = 'synapse-interface' export const persistConfig: PersistConfig = { - version: 3, // upgrade to reset cache when updated data structures throw errors + version: 4, key: storageKey, storage, whitelist: Object.keys(persistedReducers), + migrate: migratePersistedState, } export const appReducer = combineReducers({ diff --git a/packages/synapse-interface/store/store.ts b/packages/synapse-interface/store/store.ts index d8cdf3e70a..a94362478c 100644 --- a/packages/synapse-interface/store/store.ts +++ b/packages/synapse-interface/store/store.ts @@ -16,7 +16,11 @@ const checkVersionAndResetCache = (): boolean => { const persistedState = JSON.parse(persistedStateRaw) const persistedVersion = JSON.parse(persistedState._persist) - if (persistedVersion.version !== persistConfig.version) { + // Version 3 has a targeted migration that preserves unrelated activity. + if ( + persistedVersion.version !== persistConfig.version && + !(persistedVersion.version === 3 && persistConfig.version === 4) + ) { return true } } diff --git a/packages/synapse-interface/utils/getPendingBridgeTransactionTrackingData.ts b/packages/synapse-interface/utils/getPendingBridgeTransactionTrackingData.ts index c85126a3e6..c0b2b01764 100644 --- a/packages/synapse-interface/utils/getPendingBridgeTransactionTrackingData.ts +++ b/packages/synapse-interface/utils/getPendingBridgeTransactionTrackingData.ts @@ -1,7 +1,6 @@ import { zeroAddress } from 'viem' import { HYPERLIQUID } from '@/constants/chains/master' -import { isHyperliquidUsdcDeposit } from '@/utils/hyperliquid' import { PendingBridgeTransaction } from '@/slices/transactions/actions' export const getPendingBridgeTransactionTrackingData = ( @@ -17,15 +16,9 @@ export const getPendingBridgeTransactionTrackingData = ( tx.destinationChain?.chainSymbol === HYPERLIQUID.chainSymbol ? HYPERLIQUID : tx.destinationChain - const isHyperliquidDeposit = - !tx.bridgeModuleName && - isHyperliquidUsdcDeposit( - destinationChain?.id, - tx.destinationToken ?? tx.originToken - ) const hasTrackedTimestamp = typeof tx.timestamp === 'number' - if (!hasTrackedTimestamp && !isHyperliquidDeposit) { + if (!hasTrackedTimestamp) { return null } @@ -41,7 +34,7 @@ export const getPendingBridgeTransactionTrackingData = ( bridgeModuleName: tx.bridgeModuleName ?? '', routerAddress: tx.routerAddress ?? zeroAddress, estimatedTime: tx.estimatedTime ?? 0, - timestamp: tx.timestamp ?? tx.id, + timestamp: tx.timestamp, status: 'pending' as const, } } diff --git a/packages/synapse-interface/utils/hyperliquid.ts b/packages/synapse-interface/utils/hyperliquid.ts deleted file mode 100644 index c587fd1687..0000000000 --- a/packages/synapse-interface/utils/hyperliquid.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master' - -// The existing Hyperliquid destination uses an Arbitrum deposit only for USDC. -// SYN reaches HyperCore directly through the OFT composer. -export const isHyperliquidUsdcDeposit = ( - toChainId: number, - toToken?: { routeSymbol?: string } -) => toChainId === HYPERLIQUID.id && toToken?.routeSymbol === 'USDC' - -export const getBridgeDestinationChainId = ( - toChainId: number, - toToken?: { routeSymbol?: string } -) => (isHyperliquidUsdcDeposit(toChainId, toToken) ? ARBITRUM.id : toChainId)