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.
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 HookexportfunctionuseLiquidityHubSDK() {const { chainId } =useAccount()returnuseMemo(() =>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 secondsexportconstQUOTE_REFETCH_INTERVAL=20_000exportfunctionuseLiquidityHubQuote(args:QuoteArgs) {// Get the SDK from the hook we implementedconstliquidityHub=useLiquidityHubSDK()// Get the react-query client as we will use this to provide a function// to get the latest quote from react-query cache.constqueryClient=useQueryClient()// Get the chainId of currently selected network in wallet using wagmi hookconst { chainId } =useAccount()...}
Next, define a callback for the SDK getQuote function
constgetQuote=useCallback( ({ signal }: { signal:AbortSignal }) => {constpayload:QuoteArgs= {...args, fromToken:resolveNativeTokenAddress(args.fromToken), }returnliquidityHub.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.
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'exportconstQUOTE_REFETCH_INTERVAL=20_000// Fetches quote using Liquidity Hub sdkexportfunctionuseLiquidityHubQuote(args:QuoteArgs, disabled?:boolean) {constliquidityHub=useLiquidityHubSDK()constqueryClient=useQueryClient()const { chainId } =useAccount()// Check if the swap is wrap or unwrap onlyconst { isUnwrapOnly,isWrapOnly } =useWrapOrUnwrapOnly(args.fromToken,args.toToken )// Flag to determine whether to getQuoteconstenabled=Boolean(!disabled && chainId &&args.fromToken &&args.toToken &&Number(args.inAmount) >0&&!isUnwrapOnly &&!isWrapOnly )constqueryKey=useMemo( () => ['quote',args.fromToken,args.toToken,args.inAmount,args.slippage], [args.fromToken,args.inAmount,args.slippage,args.toToken] )// Callback to call Liquidity Hub sdk getQuoteconstgetQuote=useCallback( ({ signal }: { signal:AbortSignal }) => {constpayload:QuoteArgs= {...args, fromToken:resolveNativeTokenAddress(args.fromToken)!, }// The abort signal is optionalreturnliquidityHub.getQuote({ ...payload, signal }) }, [liquidityHub, args] )// result from getQuoteconstquery=useQuery({ queryKey, queryFn: getQuote, enabled, refetchOnWindowFocus:false, staleTime:Infinity, gcTime:0, retry:2, refetchInterval:QUOTE_REFETCH_INTERVAL, })returnuseMemo(() => {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.
constliquidityProvider=useMemo(() => {// Choose between liquidity hub and dex swap based on the min amount outif ( (!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:
Wrap the native token
Approve the allowance for the input token
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.
exportfunctionuseLiquidityHubSwapCallback() {constliquidityHub=useLiquidityHubSDK()// We want to build the existing router's tx so we can send to// Liquidity Hub to make comparisonsconstbuildParaswapTxCallback=useParaswapBuildTxCallback()// Get the connected walletconstaccount=useAccount()returnuseMutation({mutationFn:async ({ inTokenAddress, optimalRate, slippage, getQuote, onAcceptQuote, onSuccess, onFailure, onSettled, }: { inTokenAddress:string optimalRate:OptimalRate slippage:numbergetQuote: () =>Promise<Quote> onAcceptQuote: (quote:Quote) =>voidonSuccess?: () =>voidonFailure?: () =>voidonSettled?: () =>void }) => {// Fetch latest quote just before swapconstquote=awaitgetQuote()... } })}
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.
exportfunctionuseLiquidityHubSwapCallback() {...if (isNativeAddress(inTokenAddress)) {awaitwrapToken(quote) }...}asyncfunctionwrapToken(quote:Quote) {try {// Simulate the contract to check if there would be any errorsconstsimulatedData=awaitsimulateContract(wagmiConfig, { abi: IWETHabi, functionName:'deposit', account:quote.user asAddress, address:NATIVE_WRAPPED_TOKEN_ADDRESSasAddress, value:BigInt(quote.inAmount), })// Perform the deposit contract functionconsttxHash=awaitwriteContract(wagmiConfig,simulatedData.request)// Check for confirmations for a maximum of 20 secondsawaitwaitForConfirmations(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'exportfunctionuseLiquidityHubSwapCallback() {...// Check if the inToken needs approval for allowanceconst { requiresApproval } =awaitgetRequiresApproval( permit2Address,resolveNativeTokenAddress(inTokenAddress),quote.inAmount,account.address asstring ) if (requiresApproval) {awaitapproveAllowance(quote.user,quote.inToken) }...}asyncfunctionapproveAllowance( account:string, inToken:string,) {try {// Simulate the contract to check if there would be any errorsconstsimulatedData=awaitsimulateContract(wagmiConfig, { abi: erc20Abi, functionName:'approve', args: [permit2Address, maxUint256], account: account asAddress, address: (isNativeAddress(inToken)?NATIVE_WRAPPED_TOKEN_ADDRESS: inToken) asAddress, })// Perform the approve contract functionconsttxHash=awaitwriteContract(wagmiConfig,simulatedData.request)// Check for confirmations for a maximum of 20 secondsawaitwaitForConfirmations(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'exportfunctionuseLiquidityHubSwapCallback() {...// Fetch the latest quote again after the approvalconstlatestQuote=awaitgetQuote()onAcceptQuote(latestQuote)// Sign the transaction for the swapconstsignature=awaitsignTransaction(latestQuote)...}asyncfunctionsignTransaction(quote:Quote) {// Encode the payload to get signatureconst { permitData } = quoteconstpopulated=await_TypedDataEncoder.resolveNames(permitData.domain,permitData.types,permitData.values,async (name:string) => name )constpayload=_TypedDataEncoder.getPayload(populated.domain,permitData.types,populated.value )try {// Sign transaction and get signatureconstsignature=awaitpromiseWithTimeout(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.
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.
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.
constswapWithLiquidityHub=useCallback(async () => {try {awaitliquidityHubSwapCallback({ 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 swapconsole.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.
Here’s an example of how to apply analytics to the wrap function:
asyncfunctionwrapToken(quote:Quote, liquidityHubSDK:LiquidityHubSDK) {try {liquidityHubSDK.analytics.onWrapRequest();// Simulate the contract to check if there would be any errorsconstsimulatedData=awaitsimulateContract(wagmiConfig, { abi: IWETHabi, functionName:"deposit", account:quote.user asAddress, address:networks.poly.wToken.address asAddress, value:BigInt(quote.inAmount), });// Perform the deposit contract functionconsttxHash=awaitwriteContract(wagmiConfig,simulatedData.request);// Check for confirmations for a maximum of 20 secondsawaitwaitForConfirmations(txHash,1,20);liquidityHubSDK.analytics.onWrapSuccess();return txHash; } catch (error) {consterrorMessage=getErrorMessage( error,"An error occurred while wrapping your token" );liquidityHubSDK.analytics.onWrapFailure(errorMessage); }}