Integrating Liquidity Hub with your DEX

This page provides a practical example of how to integrate the Orbs Liquidity Hub SDK into your React based DEX (Decentralised Exchange). By following this tutorial, developers will learn how to leverage the SDK’s features for aggregating liquidity and interacting with liquidity providers directly in their front-end code. The example demonstrates essential setup steps, key functions, and best practices for utilising the SDK within a modern React environment, making it easier to implement and customise liquidity solutions in your own dApp.

You can explore the full source code of an example here.

Initialise the SDK

After installing the SDK (see instructions here), you will need to initialise it.

import { constructSDK } from '@orbs-network/liquidity-hub-sdk'
import { useMemo } from 'react'
import { useAccount } from 'wagmi'

// Initialise Liquidity Hub sdk and provide in a React Hook
export function useLiquidityHubSDK() {
  const { chainId } = useAccount()
  return useMemo(() => constructSDK({ partner: 'widget', chainId }), [chainId])
}

This will enable you to use the SDK throughout your React app.

Note: You may need to reach out to us to obtain the partner parameter.

Get a quote

To get a quote for a swap, you will need a React Hook that handles the call to the getQuote function of the SDK.

We recommend using React Query as it is the best library to handle Promises. Its continuous refetch feature ensures the quote is always up to date.

The getQuote SDK function takes several arguments (QuoteArgs), which you will need to pass in from your dApp. See documentation on the arguments here.

// Define a refetch interval of 10 seconds
export const QUOTE_REFETCH_INTERVAL = 20_000

export function useLiquidityHubQuote(args: QuoteArgs) {
  // Get the SDK from the hook we implemented
  const liquidityHub = useLiquidityHubSDK()
  
  // Get the react-query client as we will use this to provide a function
  // to get the latest quote from react-query cache.
  const queryClient = useQueryClient()
  
  // Get the chainId of currently selected network in wallet using wagmi hook
  const { chainId } = useAccount()

  ...
}

Next, define a callback for the SDK getQuote function

const getQuote = useCallback(
  ({ signal }: { signal: AbortSignal }) => {
    const payload: QuoteArgs = {
      ...args,
      fromToken: resolveNativeTokenAddress(args.fromToken),
    }
    return liquidityHub.getQuote({ ...payload, signal })
  },
  [liquidityHub, args]
)

Note: resolveNativeTokenAddress is a util function that returns the wrapped version of the native token address.

We pass in the quote args, replacing the input token with the wrapped version when getting a quote.

Next, define the React Query hook for the getQuote callback.

const query = useQuery({
  queryKey: [
    'quote',
    args.fromToken,
    args.toToken,
    args.inAmount,
    args.slippage,
  ],
  queryFn: getQuote,
  enabled,
  refetchOnWindowFocus: false,
  staleTime: Infinity,
  gcTime: 0,
  retry: 2,
  refetchInterval: QUOTE_REFETCH_INTERVAL,
})

Finally, return the query result and a function to get the last quote fetched from cache:

return useMemo(() => {
  return {
    ...query,
    getLatestQuote: () =>
      queryClient.ensureQueryData({
        queryKey,
        queryFn: getQuote,
      }),
  }
}, [query, queryClient, queryKey, getQuote])

The full useLiquidityHubQuote should look something like this:

import { QuoteArgs } from '@orbs-network/liquidity-hub-sdk'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useLiquidityHubSDK } from './useLiquidityHubSDK'
import { useAccount } from 'wagmi'
import { useCallback, useMemo } from 'react'
import { networks, isNativeAddress, useWrapOrUnwrapOnly } from '@/lib'

export const QUOTE_REFETCH_INTERVAL = 20_000

// Fetches quote using Liquidity Hub sdk
export function useLiquidityHubQuote(args: QuoteArgs, disabled?: boolean) {
  const liquidityHub = useLiquidityHubSDK()
  const queryClient = useQueryClient()
  const { chainId } = useAccount()

  // Check if the swap is wrap or unwrap only
  const { isUnwrapOnly, isWrapOnly } = useWrapOrUnwrapOnly(
    args.fromToken,
    args.toToken
  )

  // Flag to determine whether to getQuote
  const enabled = Boolean(
    !disabled &&
    chainId &&
      args.fromToken &&
      args.toToken &&
      Number(args.inAmount) > 0 &&
      !isUnwrapOnly &&
      !isWrapOnly
  )

  const queryKey = useMemo(
    () => ['quote', args.fromToken, args.toToken, args.inAmount, args.slippage],
    [args.fromToken, args.inAmount, args.slippage, args.toToken]
  )

  // Callback to call Liquidity Hub sdk getQuote
  const getQuote = useCallback(
    ({ signal }: { signal: AbortSignal }) => {
      const payload: QuoteArgs = {
        ...args,
        fromToken: resolveNativeTokenAddress(args.fromToken)!,
      }
      
      // The abort signal is optional
      return liquidityHub.getQuote({ ...payload, signal })
    },
    [liquidityHub, args]
  )

  // result from getQuote
  const query = useQuery({
    queryKey,
    queryFn: getQuote,
    enabled,
    refetchOnWindowFocus: false,
    staleTime: Infinity,
    gcTime: 0,
    retry: 2,
    refetchInterval: QUOTE_REFETCH_INTERVAL,
  })

  return useMemo(() => {
    return {
      // We return the result of getQuote, plus a function to get
      // the last fetched quote in react-query cache
      ...query,
      getLatestQuote: () =>
        queryClient.ensureQueryData({
          queryKey,
          queryFn: getQuote,
        }),
    }
  }, [query, queryClient, queryKey, getQuote])
}

This hook can now be used in your dApp to continuously provide a quote based on the input, which can then be compared to your existing quote to determine which offers the user the best price.

Compare Liquidity Hub Quote to your existing provider

We have designed Liquidity Hub to work seamlessly alongside your existing liquidity providers. You can easily handle this in your dApp by comparing the quotes from Liquidity Hub with those of your existing provider.

Here's an example of how you can implement this in your dApp. First, create a memoized value for which liquidity provider you want to use by comparing the minimum amount out from your current provider with Liquidity Hub . Then, in the onClick callback for the confirm swap button, proceed with the best liquidity provider.

const liquidityProvider = useMemo(() => {
  // Choose between liquidity hub and dex swap based on the min amount out
  if (
    (!liquidityHubDisabled &&
      toBigInt(liquidityHubQuote?.minAmountOut || 0) >
        BigInt(paraswapMinAmountOut || 0)
  ) {
    return 'liquidityhub'
  }

  return 'paraswap'
}, [
  forceLiquidityHub,
  liquidityHubDisabled,
  liquidityHubQuote?.minAmountOut,
  paraswapMinAmountOut,
])

const confirmSwap = useCallback(async () => {
  if (liquidityProvider === 'liquidityhub') {
    console.log('Proceeding with Liquidity Hub')
    swapWithLiquidityHub()
  } else {
    console.log('Proceeding with ParaSwap')
    setLiquidityHubDisabled(true)
    swapWithParaswap()
  }
}, [proceedWithLiquidityHubSwap])

In this example, we are using ParaSwap as our existing liquidity provider.

Swap flow

Now that we've fetched a quote and confirmed that Liquidity Hub provides a better trade for the user, we want to perform a Liquidity Hub swap and handle all the steps.

There are three possible steps involved in a swap:

  1. Wrap the native token

  2. Approve the allowance for the input token

  3. Perform the swap

All steps require the user to confirm with their wallet before proceeding.

First, we'll create a hook that returns a mutation function to call from the UI.

export function useLiquidityHubSwapCallback() {
  const liquidityHub = useLiquidityHubSDK()
  
  // We want to build the existing router's tx so we can send to
  // Liquidity Hub to make comparisons
  const buildParaswapTxCallback = useParaswapBuildTxCallback()
  
  // Get the connected wallet
  const account = useAccount()
  
  return useMutation({
    mutationFn: async ({
      inTokenAddress,
      optimalRate,
      slippage,
      getQuote,
      onAcceptQuote,
      onSuccess,
      onFailure,
      onSettled,
    }: {
      inTokenAddress: string
      optimalRate: OptimalRate
      slippage: number
      getQuote: () => Promise<Quote>      
      onAcceptQuote: (quote: Quote) => void
      onSuccess?: () => void
      onFailure?: () => void
      onSettled?: () => void
    }) => {
      // Fetch latest quote just before swap
      const quote = await getQuote()
      
      ...
      
    }
  })
}

Arguments for the Swap Callback:

  • inTokenAddress: The contract address for the input token.

  • optimalRate: The quote returned from your existing liquidity provider, in this example we use ParaSwap.

  • slippage: The defined slippage.

  • getQuote: A function that fetches the latest quote.

  • onAcceptQuote: A callback that passes the final quote before the swap is made. The quote is stored in React state to ensure the final quote is displayed to the user.

  • onSuccess, onFailure and onSettled: Optional callback functions triggered during the swap process.

Wrap the token

Before executing the swap, we need to check if the token requires wrapping.

export function useLiquidityHubSwapCallback() {
  ...
  if (isNativeAddress(inTokenAddress)) {
    await wrapToken(quote)
  }
  ...
}

async function wrapToken(quote: Quote) {
  try {
    // Simulate the contract to check if there would be any errors
    const simulatedData = await simulateContract(wagmiConfig, {
      abi: IWETHabi,
      functionName: 'deposit',
      account: quote.user as Address,
      address: NATIVE_WRAPPED_TOKEN_ADDRESS as Address,
      value: BigInt(quote.inAmount),
    })

    // Perform the deposit contract function
    const txHash = await writeContract(wagmiConfig, simulatedData.request)

    // Check for confirmations for a maximum of 20 seconds
    await waitForConfirmations(txHash, 1, 20)

  } catch (error) {
    console.error(error)
  }
}

In the wrapToken function, we use Wagmi actions to simulate the contract call and detect any errors before executing the actual wrapping. This helps save gas by preventing a failed wrap attempt. If any errors occur during the simulation, the wrapping process will not proceed.

When the writeContract function is called, the user is prompted by their connected wallet to approve the wrapping of the native token.

The waitForConfirmations function is a utility that uses another Wagmi action to monitor the number of confirmations over a specified period.

Approve allowance

Next, we need to get the user to approve an allowance for the input token to be used in the swap if they haven’t already done so. You may already have an implementation for this step that you can use.

import { permit2Address } from '@orbs-network/liquidity-hub-sdk'



export function useLiquidityHubSwapCallback() {
  ...
  // Check if the inToken needs approval for allowance
  const { requiresApproval } = await getRequiresApproval(
    permit2Address,
    resolveNativeTokenAddress(inTokenAddress),
    quote.inAmount,
    account.address as string
  )  
  
  if (requiresApproval) {
    await approveAllowance(quote.user, quote.inToken)
  }
  ...
}

async function approveAllowance(
  account: string,
  inToken: string,
) {
  try {

    // Simulate the contract to check if there would be any errors
    const simulatedData = await simulateContract(wagmiConfig, {
      abi: erc20Abi,
      functionName: 'approve',
      args: [permit2Address, maxUint256],
      account: account as Address,
      address: (isNativeAddress(inToken)
        ? NATIVE_WRAPPED_TOKEN_ADDRESS
        : inToken) as Address,
    })

    // Perform the approve contract function
    const txHash = await writeContract(wagmiConfig, simulatedData.request)

    // Check for confirmations for a maximum of 20 seconds
    await waitForConfirmations(txHash, 1, 20)

  } catch (error) {
    console.error(error)
  }
}

We follow a similar process for approving the allowance as we did for wrapping the token. First, we simulate the contract call to check for potential errors. Then, we call the writeContract Wagmi action.

Note: The arguments for the contract call are permit2Address and maxUint256.

  • permit2Address comes from the Liquidity Hub SDK package.

  • We use maxUint256 so that the user only needs to approve the allowance for the input token once. They won’t need to repeat this step in subsequent swaps, as the maximum allowance has already been approved.

You may want the user to approve an allowance for only the amount of the swap, requiring them to do this step for every swap. However, this would not provide the best user experience.

Sign the transaction

Now that we’ve completed the steps of wrapping and approving (if required), we need to have the user sign the transaction payload. The signature will then be sent to the Liquidity Hub swap function to perform the swap.

import { _TypedDataEncoder } from '@ethersproject/hash'

export function useLiquidityHubSwapCallback() {
  ...
  // Fetch the latest quote again after the approval
  const latestQuote = await getQuote()
  onAcceptQuote(latestQuote)
  
  // Sign the transaction for the swap
  const signature = await signTransaction(latestQuote)
  ...
}

async function signTransaction(quote: Quote) {
  // Encode the payload to get signature
  const { permitData } = quote
  const populated = await _TypedDataEncoder.resolveNames(
    permitData.domain,
    permitData.types,
    permitData.values,
    async (name: string) => name
  )
  const payload = _TypedDataEncoder.getPayload(
    populated.domain,
    permitData.types,
    populated.value
  )

  try {

    // Sign transaction and get signature
    const signature = await promiseWithTimeout(
      signTypedData(wagmiConfig, payload),
      40_000
    )

    return signature
  } catch (error) {
    console.error(error)
  }
}

To obtain a signature, we first need to encode the transaction payload. In this example, we use the encoding functions from the @ethersproject/hash package.

Finally, we obtain the signature using the signTypedData Wagmi action, which is then returned for use in the swap. The 40-second signature timeout is necessary because markets move quickly, and the quote can become stale.

Swap

export function useLiquidityHubSwapCallback() {
  ...      
  try {

    // Call Liquidity Hub sdk swap and wait for transaction hash
    const txHash = await liquidityHub.swap(latestQuote, signature as string)

    if (!txHash) {
      throw new Error('Swap failed')
    }

    // Fetch the successful transaction details
    const txDetails = await liquidityHub.getTransactionDetails(txHash, latestQuote)

    if (onSuccess) onSuccess()
  } catch (error) {
    console.error(error)

    if (onFailure) onFailure()
  }

  if (onSettled) onSettled()
}

To perform the swap, we pass in the latest saved quote and the signature from the previous step, then wait for the transaction hash. Once we have a transaction, we can retrieve the transaction details.

Use the event callbacks (onSuccess, onFailure and onSettled) to provide feedback to the user.

See the full source code here.

Error handling

In the event of an error, it’s important to show the user a friendly message and handle the error accordingly. We provide UI components for this in the Swap UI Library, but you can also use custom components alongside the library.

If a Liquidity Hub swap fails, you may want to fall back to your existing liquidity provider to ensure a smooth experience for the user. You can achieve this by wrapping the swap callback from the useLiquidityHubSwapCallback hook in a try-catch block and then proceeding with the existing swap method if an error occurs.

const swapWithLiquidityHub = useCallback(async () => {
  try {
    await liquidityHubSwapCallback({
      inTokenAddress: inToken!.address,
      getQuote: getLatestQuote,
      onAcceptQuote,
      setSwapStatus,
      setCurrentStep,
      onFailure: onSwapConfirmClose,
      setSignature,
    })
  } catch (error) {
    // If the liquidity hub swap fails, need to set the flag to prevent further attempts, and proceed with the dex swap
    // stop quoting from Liquidity Hub
    // start new flow with dex swap
    console.error(error)
    toast.error('Liquidity Hub swap failed, proceeding with Dex swap')
    setLiquidityHubDisabled(true)
    swapWithParaswap()
  }
}, [
  liquidityHubSwapCallback,
  inToken,
  getLatestQuote,
  onAcceptQuote,
  onSwapConfirmClose,
  swapWithParaswap,
])

In this example, we maintain a state that tracks whether Liquidity Hub should be disabled. In the event of an error, we disable it so that quotes from Liquidity Hub will no longer be fetched.

Analytics

The SDK includes analytics events that can be triggered at various stages of the swap process. These events provide valuable business insights into how Liquidity Hub swaps are performing on your decentralised exchange.

onWrapRequest: () => void;
onWrapSuccess: () => void;
onWrapFailure: (error: string) => void;
onApprovalRequest: () => void;
onApprovalFailed: (error: string) => void;
onApprovalSuccess: (approvalTxHash?: string) => void;
onSignatureSuccess: (signature: string) => void;
onSignatureRequest: () => void;
onSignatureFailed: (error: string) => void;

Here’s an example of how to apply analytics to the wrap function:

async function wrapToken(quote: Quote, liquidityHubSDK: LiquidityHubSDK) {
  try {

    liquidityHubSDK.analytics.onWrapRequest();
    
    // Simulate the contract to check if there would be any errors
    const simulatedData = await simulateContract(wagmiConfig, {
      abi: IWETHabi,
      functionName: "deposit",
      account: quote.user as Address,
      address: networks.poly.wToken.address as Address,
      value: BigInt(quote.inAmount),
    });

    // Perform the deposit contract function
    const txHash = await writeContract(wagmiConfig, simulatedData.request);

    // Check for confirmations for a maximum of 20 seconds
    await waitForConfirmations(txHash, 1, 20);

    liquidityHubSDK.analytics.onWrapSuccess();

    return txHash;
  } catch (error) {
    const errorMessage = getErrorMessage(
      error,
      "An error occurred while wrapping your token"
    );

    liquidityHubSDK.analytics.onWrapFailure(errorMessage);

  }
}

Last updated