Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(synapse-interface): Adds Hyperliquid bridge & deposit support #3461

Merged
merged 13 commits into from
Dec 13, 2024
3 changes: 3 additions & 0 deletions packages/synapse-interface/assets/chains/hyperliquid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Image from 'next/image'
import { CheckCircleIcon } from '@heroicons/react/outline'

import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master'

export const HyperliquidDepositInfo = ({
fromChainId,
isOnArbitrum,
hasDepositedOnHyperliquid,
}) => {
Comment on lines +3 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript types to component props

Adding TypeScript types will improve type safety and developer experience:

-export const HyperliquidDepositInfo = ({
+interface HyperliquidDepositInfoProps {
+  fromChainId: number;
+  isOnArbitrum: boolean;
+  hasDepositedOnHyperliquid: boolean;
+}
+
+export const HyperliquidDepositInfo: React.FC<HyperliquidDepositInfoProps> = ({
   fromChainId,
   isOnArbitrum,
   hasDepositedOnHyperliquid,
 }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const HyperliquidDepositInfo = ({
fromChainId,
isOnArbitrum,
hasDepositedOnHyperliquid,
}) => {
interface HyperliquidDepositInfoProps {
fromChainId: number;
isOnArbitrum: boolean;
hasDepositedOnHyperliquid: boolean;
}
export const HyperliquidDepositInfo: React.FC<HyperliquidDepositInfoProps> = ({
fromChainId,
isOnArbitrum,
hasDepositedOnHyperliquid,
}) => {

return (
<div className="flex flex-col p-2 mb-2 space-y-1 text-sm border rounded border-zinc-300 dark:border-separator">
<div className="flex items-center mb-1 space-x-2 ">
<Image
loading="lazy"
src={HYPERLIQUID.chainImg}
alt="Switch Network"
width="16"
height="16"
className="w-4 h-4 max-w-fit"
/>

<div>Hyperliquid Deposit</div>
</div>
<div className="flex items-center space-x-2">
<CheckCircleIcon
className={`w-3 h-3 ${
fromChainId === ARBITRUM.id && isOnArbitrum
? 'text-green-500'
: 'text-gray-500'
}`}
/>
<div>Bridge to Arbitrum</div>
</div>
<div className="flex items-center space-x-2">
<CheckCircleIcon
className={`w-3 h-3 ${
hasDepositedOnHyperliquid ? 'text-green-500' : 'text-gray-500'
}`}
/>
<div>Deposit to Hyperliquid</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { useEffect, useState } from 'react'
import { useAccount, useAccountEffect, useSwitchChain } from 'wagmi'
import { useConnectModal } from '@rainbow-me/rainbowkit'
import { useTranslations } from 'next-intl'
import { Address, 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'

const HYPERLIQUID_DEPOSIT_ADDRESS = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move sensitive deposit address to environment configuration

The Hyperliquid deposit address should not be hardcoded in the source code. This makes it difficult to manage across different environments and poses security risks.

Consider:

  1. Moving this to environment variables or a secure configuration management system
  2. Adding address checksum validation
  3. Including a comment documenting the address purpose and ownership
-const HYPERLIQUID_DEPOSIT_ADDRESS = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7'
+const HYPERLIQUID_DEPOSIT_ADDRESS = process.env.NEXT_PUBLIC_HYPERLIQUID_DEPOSIT_ADDRESS
+if (!HYPERLIQUID_DEPOSIT_ADDRESS || !isAddress(HYPERLIQUID_DEPOSIT_ADDRESS)) {
+  throw new Error('Invalid Hyperliquid deposit address configuration')
+}

Committable suggestion skipped: line range outside the PR's diff.


const approve = async (address: Address, amount: bigint) => {
const { request } = await simulateContract(wagmiConfig, {
chainId: ARBITRUM.id,
address: USDC.addresses[ARBITRUM.id],
abi: erc20Abi,
functionName: 'approve',
args: [address, amount],
})

const hash = await writeContract(wagmiConfig, request)

const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash })

return txReceipt
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling in approve function

The approve function should include proper error handling and validation.

 const approve = async (address: Address, amount: bigint) => {
+  if (!address || amount <= 0n) {
+    throw new Error('Invalid approve parameters')
+  }
+
   const { request } = await simulateContract(wagmiConfig, {
     chainId: ARBITRUM.id,
     address: USDC.addresses[ARBITRUM.id],
     abi: erc20Abi,
     functionName: 'approve',
     args: [address, amount],
   })

   const hash = await writeContract(wagmiConfig, request)

   const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash })

   return txReceipt
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const approve = async (address: Address, amount: bigint) => {
const { request } = await simulateContract(wagmiConfig, {
chainId: ARBITRUM.id,
address: USDC.addresses[ARBITRUM.id],
abi: erc20Abi,
functionName: 'approve',
args: [address, amount],
})
const hash = await writeContract(wagmiConfig, request)
const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash })
return txReceipt
}
const approve = async (address: Address, amount: bigint) => {
if (!address || amount <= 0n) {
throw new Error('Invalid approve parameters')
}
const { request } = await simulateContract(wagmiConfig, {
chainId: ARBITRUM.id,
address: USDC.addresses[ARBITRUM.id],
abi: erc20Abi,
functionName: 'approve',
args: [address, amount],
})
const hash = await writeContract(wagmiConfig, request)
const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash })
return txReceipt
}


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
}
}
Comment on lines +29 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Enhance deposit function with proper validation and error handling

The deposit function needs additional safeguards for this critical financial operation.

 const deposit = async (amount: bigint) => {
+  if (!amount || amount <= 0n) {
+    throw new Error('Invalid deposit amount')
+  }
+
   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 timeout = setTimeout(() => {
+      throw new Error('Transaction confirmation timeout')
+    }, 300000) // 5 minutes timeout
+
     const txReceipt = await waitForTransactionReceipt(wagmiConfig, { hash })
+    clearTimeout(timeout)

     return txReceipt
   } catch (error) {
-    console.error('Confirmation error:', error)
+    const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+    console.error('Deposit transaction failed:', errorMessage)
+    throw new Error(`Deposit failed: ${errorMessage}`)
   }
 }

Committable suggestion skipped: line range outside the PR's diff.


export const HyperliquidTransactionButton = ({
isTyping,
hasDepositedOnHyperliquid,
setHasDepositedOnHyperliquid,
}) => {
const [isApproved, setIsApproved] = useState(false)
const [isApproving, setIsApproving] = useState(false)
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) >= 5

const t = useTranslations('Bridge')

const amount = stringToBigInt(
debouncedFromValue,
fromToken.decimals[fromChainId]
)

const handleApprove = async () => {
setIsApproving(true)

try {
await approve(address, amount)
setIsApproved(true)
} catch (error) {
console.error('Approval error:', error)
} finally {
setIsApproving(false)
}
}

const handleDeposit = async () => {
setIsDepositing(true)
const currentTimestamp: number = getUnixTimeMinutesFromNow(0)
try {
const txReceipt = await deposit(amount)

setHasDepositedOnHyperliquid(true)
setIsApproved(false)
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)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance transaction error handling and user feedback

The deposit handler could benefit from more detailed error handling and user feedback.

 const handleDeposit = async () => {
   setIsDepositing(true)
   const currentTimestamp: number = getUnixTimeMinutesFromNow(0)
   try {
     const txReceipt = await deposit(amount)

     setHasDepositedOnHyperliquid(true)
     setIsApproved(false)
     segmentAnalyticsEvent(`[Hyperliquid Deposit]`, {
       inputAmount: debouncedFromValue,
     })
     // ... dispatch calls ...
   } catch (error) {
-    console.error('Deposit error:', error)
+    console.error('Deposit error:', error)
+    segmentAnalyticsEvent(`[Hyperliquid Deposit Error]`, {
+      inputAmount: debouncedFromValue,
+      error: error.message
+    })
+    throw new Error('Failed to complete deposit. Please try again.')
   } finally {
     setIsDepositing(false)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDeposit = async () => {
setIsDepositing(true)
const currentTimestamp: number = getUnixTimeMinutesFromNow(0)
try {
const txReceipt = await deposit(amount)
setHasDepositedOnHyperliquid(true)
setIsApproved(false)
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)
}
}
const handleDeposit = async () => {
setIsDepositing(true)
const currentTimestamp: number = getUnixTimeMinutesFromNow(0)
try {
const txReceipt = await deposit(amount)
setHasDepositedOnHyperliquid(true)
setIsApproved(false)
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)
segmentAnalyticsEvent(`[Hyperliquid Deposit Error]`, {
inputAmount: debouncedFromValue,
error: error.message
})
throw new Error('Failed to complete deposit. Please try again.')
} finally {
setIsDepositing(false)
}
}


useAccountEffect({
onDisconnect() {
setIsConnected(false)
},
})

useEffect(() => {
setIsConnected(isConnectedInit)
}, [isConnectedInit])

const isButtonDisabled =
isTyping ||
isApproving ||
isDepositing ||
!depositingMinimumAmount ||
isWalletPending ||
!hasValidInput ||
(isConnected && !hasSufficientBalance)

let buttonProperties

if (isConnected && !hasSufficientBalance) {
buttonProperties = {
label: t('Insufficient balance'),
onClick: null,
}
} else if (!depositingMinimumAmount) {
buttonProperties = {
label: '5 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 if (!isApproved && hasValidInput) {
buttonProperties = {
onClick: handleApprove,
label: t('Approve {symbol}', { symbol: fromToken?.symbol }),
pendingLabel: t('Approving'),
}
} else {
buttonProperties = {
onClick: handleDeposit,
label: t('Deposit {symbol}', { symbol: fromToken?.symbol }),
pendingLabel: t('Depositing'),
}
}

return (
buttonProperties && (
<>
<div className="flex flex-col w-full">
<TransactionButton
{...buttonProperties}
disabled={isButtonDisabled}
chainId={fromChainId}
/>
</div>
</>
Comment on lines +176 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unnecessary Fragment to simplify JSX structure

The Fragment (<> and </>) wrapping the <div> is redundant since it contains only one child. Removing it will simplify the JSX structure without affecting functionality.

Apply the following diff to remove the unnecessary Fragment:

      return (
        buttonProperties && (
-          <>
            <div className="flex flex-col w-full">
              <TransactionButton
                {...buttonProperties}
                disabled={isButtonDisabled}
                chainId={fromChainId}
              />
            </div>
-          </>
        )
      )

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 213-221: Avoid using unnecessary Fragment.

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.
Unsafe fix: Remove the Fragment

(lint/complexity/noUselessFragments)

)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useWalletState } from '@/slices/wallet/hooks'
import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { useTranslations } from 'next-intl'
import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master'

interface OutputContainerProps {
isQuoteStale: boolean
Expand All @@ -26,6 +27,7 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
const { bridgeQuote, isLoading } = useBridgeQuoteState()
const { showDestinationAddress } = useBridgeDisplayState()
const { hasValidInput, hasValidQuote } = useBridgeValidations()
const { debouncedFromValue, fromChainId, toChainId } = useBridgeState()

const showValue = useMemo(() => {
if (!hasValidInput) {
Expand All @@ -43,7 +45,7 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
<BridgeSectionContainer>
<div className="flex items-center justify-between">
<ToChainSelector />
{showDestinationAddress ? (
{showDestinationAddress && toChainId !== HYPERLIQUID.id ? (
<DestinationAddressInput connectedAddress={address} />
) : null}
</div>
Expand All @@ -52,7 +54,11 @@ export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
<ToTokenSelector />
<AmountInput
disabled={true}
showValue={showValue}
showValue={
fromChainId === ARBITRUM.id && toChainId === HYPERLIQUID.id
? debouncedFromValue
: showValue
}
isLoading={isLoading}
className={inputClassName}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BridgeQuoteState } from '@/slices/bridgeQuote/reducer'
import { EMPTY_BRIDGE_QUOTE } from '@/constants/bridge'
import { hasOnlyZeroes } from '@/utils/hasOnlyZeroes'
import { useBridgeSelections } from './useBridgeSelections'
import { ARBITRUM, HYPERLIQUID } from '@/constants/chains/master'

export const useBridgeValidations = () => {
const { chainId } = useAccount()
Expand Down Expand Up @@ -66,7 +67,7 @@ export const useBridgeValidations = () => {
debouncedFromValue,
fromChainId,
fromToken,
toChainId,
toChainId === HYPERLIQUID.id ? ARBITRUM.id : toChainId,
toToken
)
}, [debouncedFromValue, fromChainId, fromToken, toChainId, toToken])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { RightArrow } from '@/components/icons/RightArrow'
import { Address } from 'viem'
import { useIsTxReverted } from './helpers/useIsTxReverted'
import { useTxRefundStatus } from './helpers/useTxRefundStatus'
import { HYPERLIQUID } from '@/constants/chains/master'

interface _TransactionProps {
connectedAddress: string
Expand Down Expand Up @@ -185,13 +186,15 @@ export const _Transaction = ({
iconUrl={originChain?.explorerImg}
/>
)}
{!isNull(destExplorerAddressLink) && !isTxReverted && (
<MenuItem
text={destExplorerName}
link={destExplorerAddressLink}
iconUrl={destinationChain?.explorerImg}
/>
)}
{destinationChain.id !== HYPERLIQUID.id &&
!isNull(destExplorerAddressLink) &&
!isTxReverted && (
<MenuItem
text={destExplorerName}
link={destExplorerAddressLink}
iconUrl={destinationChain?.explorerImg}
/>
)}
<MenuItem
text={t('Contact Support (Discord)')}
link="https://discord.gg/synapseprotocol"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer'
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'

/** TODO: Update naming once refactoring of previous Activity/Tx flow is done */
export const _Transactions = ({
Expand Down Expand Up @@ -61,7 +62,11 @@ export const _Transactions = ({
kappa={tx?.kappa}
timestamp={tx.timestamp}
currentTime={currentTime}
status={tx.status}
status={
tx.destinationChain.id === HYPERLIQUID.id
? 'completed'
: tx.status
}
disabled={isWalletPending}
/>
)
Expand Down
Loading
Loading