UNPKG

0xtrails

Version:

SDK for Trails

700 lines (633 loc) 16.9 kB
import { convertViemChainToRelayChain, createClient, getClient, MAINNET_RELAY_API, } from "@reservoir0x/relay-sdk" import type { WalletClient } from "viem" import { arbitrum, avalanche, base, berachain, blast, opBNB, bob, boba, celo, corn, cronos, cyber, degen, mainnet, flowMainnet, forma, funkiMainnet, gnosis, gravity, hemi, hychain, ink, linea, lisk, manta, mantle, metis, mint, mode, morph, optimism, plume, polygon, polygonZkEvm, ronin, redstone, sanko, scroll, sei, shape, soneium, sonic, story, superposition, superseed, swellchain, taiko, tron, unichain, worldchain, xai, zeroG, zircuit, zksync, zora, katana, abstract, ancient8, apeChain, arbitrumNova, arenaz, b3, } from "viem/chains" import { logger } from "./logger.js" export type Chain = any export type RelayQuote = any export type RelayExecuteResult = any export type RelayProgressData = any // Source: https://docs.relay.link/resources/supported-chains export const relaySupportedChains: Record<number, Chain> = { [abstract.id]: abstract, [ancient8.id]: ancient8, [apeChain.id]: apeChain, [arbitrum.id]: arbitrum, [arbitrumNova.id]: arbitrumNova, [arenaz.id]: arenaz, [avalanche.id]: avalanche, [b3.id]: b3, [base.id]: base, [berachain.id]: berachain, [blast.id]: blast, [opBNB.id]: opBNB, [bob.id]: bob, [boba.id]: boba, [celo.id]: celo, [corn.id]: corn, [cronos.id]: cronos, [cyber.id]: cyber, [degen.id]: degen, [mainnet.id]: mainnet, [flowMainnet.id]: flowMainnet, [forma.id]: forma, [funkiMainnet.id]: funkiMainnet, [gnosis.id]: gnosis, [gravity.id]: gravity, [hemi.id]: hemi, [hychain.id]: hychain, [ink.id]: ink, [linea.id]: linea, [lisk.id]: lisk, [manta.id]: manta, [mantle.id]: mantle, [metis.id]: metis, [mint.id]: mint, [mode.id]: mode, [morph.id]: morph, [optimism.id]: optimism, [plume.id]: plume, [polygon.id]: polygon, [polygonZkEvm.id]: polygonZkEvm, [ronin.id]: ronin, [redstone.id]: redstone, [sanko.id]: sanko, [scroll.id]: scroll, [sei.id]: sei, [shape.id]: shape, [soneium.id]: soneium, [sonic.id]: sonic, [story.id]: story, [superposition.id]: superposition, [superseed.id]: superseed, [swellchain.id]: swellchain, [taiko.id]: taiko, [tron.id]: tron, [unichain.id]: unichain, [worldchain.id]: worldchain, [xai.id]: xai, [zeroG.id]: zeroG, [zircuit.id]: zircuit, [zksync.id]: zksync, [zora.id]: zora, [katana.id]: katana, } getRelaySupportedChains().then((relayChains) => { createClient({ baseApiUrl: MAINNET_RELAY_API, source: "Trails", chains: relayChains.map((chain: Chain) => convertViemChainToRelayChain(chain), ), }) }) export enum RelayTradeType { EXACT_INPUT = "EXACT_INPUT", EXACT_OUTPUT = "EXACT_OUTPUT", } export interface RelayQuoteOptions { wallet: WalletClient chainId: number toChainId?: number amount: string currency: string toCurrency?: string tradeType?: RelayTradeType txs: Array<{ to: string value: string data: string }> recipient?: string slippageTolerance?: string } export interface RelayExecuteOptions { quote: RelayQuote wallet: WalletClient onProgress?: (data: RelayProgressData) => void } /** * Get a quote for a relay transaction */ export async function getRelaySDKQuote( options: RelayQuoteOptions, ): Promise<RelayQuote> { try { const client = getClient() if (!client) { throw new Error("Relay client not available") } logger.console.log("[trails-sdk] getRelaySDKQuote", options) const quote = await client.actions.getQuote({ wallet: options.wallet, chainId: options.chainId, toChainId: options.toChainId || options.chainId, amount: options.amount, currency: options.currency, toCurrency: options.toCurrency || options.currency, tradeType: options.tradeType || RelayTradeType.EXACT_OUTPUT, txs: options.txs, user: options.wallet.account!.address, recipient: options.recipient || options.wallet.account!.address, options: { slippageTolerance: options.slippageTolerance ? Math.round(Number(options.slippageTolerance) * 100 * 100).toString() : undefined, }, }) return quote } catch (error) { logger.console.error("[trails-sdk] Error getting relay quote:", error) throw error } } /** * Execute a relay transaction */ export async function relaySDKExecute( options: RelayExecuteOptions, ): Promise<RelayExecuteResult> { try { const client = getClient() if (!client) { throw new Error("Relay client not available") } logger.console.log( "[trails-sdk] relaysdkclient", client.chains, options.quote, ) const result = await client.actions.execute({ quote: options.quote, wallet: options.wallet, onProgress: options.onProgress || ((data) => { logger.console.log("[trails-sdk] Relay progress:", data) }), }) return result } catch (error) { logger.console.error( "[trails-sdk] Error executing relay transaction:", error, ) throw error } } /** * Helper function to create a simple relay transaction for a contract call */ export async function createSimpleRelayTransaction( wallet: WalletClient, contractAddress: string, callData: string, value: string, chainId: number, currency: string = "0x0000000000000000000000000000000000000000", // ETH ): Promise<RelayQuote> { const options: RelayQuoteOptions = { wallet, chainId, amount: value, currency, txs: [ { to: contractAddress, value, data: callData, }, ], } return await getRelaySDKQuote(options) } /** * Helper function to execute a simple relay transaction */ export async function executeSimpleRelayTransaction( quote: RelayQuote, wallet: WalletClient, onProgress?: RelayExecuteOptions["onProgress"], ): Promise<RelayExecuteResult> { return await relaySDKExecute({ quote, wallet, onProgress, }) } export function getTxHashFromRelayResult(result: RelayExecuteResult): string { let txHash = result?.data?.steps?.[result?.data?.steps!.length - 1]?.items?.[0] ?.txHashes?.[0]?.txHash if (!txHash) { txHash = result?.data?.steps?.[result?.data?.steps!.length - 1]?.items?.[0] ?.internalTxHashes?.[0]?.txHash } if (!txHash) { throw new Error("No transaction hash found in relay result") } return txHash } export async function getRelaySupportedChains(): Promise<Chain[]> { return Object.values(relaySupportedChains) } export async function isChainSupported(chainId: number): Promise<boolean> { return Object.keys(relaySupportedChains).includes(chainId.toString()) } export interface RelayToken { id: string symbol: string name: string contractAddress: string decimals: number chainId: number chainName: string imageUrl: string } export async function getRelaySupportedTokens(): Promise<RelayToken[]> { try { const relayChains = await fetchRelayChains() const tokens: RelayToken[] = [] relayChains.forEach((chain) => { if (!chain.disabled) { // Add native currency tokens.push({ id: chain.currency.id, symbol: chain.currency.symbol, name: chain.currency.name, contractAddress: chain.currency.address, decimals: chain.currency.decimals, chainId: chain.id, chainName: chain.displayName || chain.name, imageUrl: "", // Native currencies typically don't have logoURI }) // Add featured tokens chain.featuredTokens.forEach((token) => { tokens.push({ id: token.id, symbol: token.symbol, name: token.name, contractAddress: token.address, decimals: token.decimals, chainId: chain.id, chainName: chain.displayName || chain.name, imageUrl: token.metadata?.logoURI || "", }) }) // Add ERC20 currencies chain.erc20Currencies.forEach((token) => { tokens.push({ id: token.id, symbol: token.symbol, name: token.name, contractAddress: token.address, decimals: token.decimals, chainId: chain.id, chainName: chain.displayName || chain.name, imageUrl: token.metadata?.logoURI || "", }) }) // Add solver currencies (fallback for chains that might not have featuredTokens/erc20Currencies) chain.solverCurrencies.forEach((token) => { tokens.push({ id: token.id, symbol: token.symbol, name: token.name, contractAddress: token.address, decimals: token.decimals, chainId: chain.id, chainName: chain.displayName || chain.name, imageUrl: token.metadata?.logoURI || "", }) }) } }) // Remove duplicates by chainId and contractAddress const uniqueTokens = tokens.filter( (token, index, self) => index === self.findIndex( (t) => t.chainId === token.chainId && t.contractAddress.toLowerCase() === token.contractAddress.toLowerCase(), ), ) logger.console.log( `[trails-sdk] Fetched ${uniqueTokens.length} unique tokens from Relay API`, ) return uniqueTokens } catch (error) { logger.console.error( "[trails-sdk] Error fetching Relay supported tokens:", error, ) return [] } } // Types for Relay API response interface RelayApiToken { id: string symbol: string name: string address: string decimals: number metadata?: { logoURI?: string } supportsBridging?: boolean withdrawalFee?: number depositFee?: number surgeEnabled?: boolean supportsPermit?: boolean } interface RelayChain { id: number name: string displayName: string disabled: boolean currency: { id: string symbol: string name: string address: string decimals: number supportsBridging: boolean } featuredTokens: RelayApiToken[] erc20Currencies: RelayApiToken[] solverCurrencies: RelayApiToken[] } interface RelayChainsResponse { chains: RelayChain[] } // Cache for chains data let cachedChains: RelayChain[] | null = null let cacheTimestamp: number = 0 const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes async function fetchRelayChains(): Promise<RelayChain[]> { const now = Date.now() // Return cached data if still valid if (cachedChains && now - cacheTimestamp < CACHE_DURATION) { return cachedChains } try { const response = await fetch("https://api.relay.link/chains") if (!response.ok) { throw new Error(`Failed to fetch chains: ${response.status}`) } const data: RelayChainsResponse = await response.json() cachedChains = data.chains cacheTimestamp = now return data.chains } catch (error) { logger.console.error("[trails-sdk] Error fetching Relay chains:", error) // Return cached data if available, even if expired if (cachedChains) { return cachedChains } throw error } } export type GetIsRouteSupportedOptions = { originChainId: number destinationChainId: number amount: string originToken: string destinationToken: string } export async function getIsRouteSupported({ originChainId, destinationChainId, amount, originToken, destinationToken, }: GetIsRouteSupportedOptions): Promise<boolean> { try { // Fetch supported chains from Relay API const chains = await fetchRelayChains() // Check if both chains are supported and not disabled const originChain = chains.find((chain) => chain.id === originChainId) const destinationChain = chains.find( (chain) => chain.id === destinationChainId, ) if ( !originChain || !destinationChain || originChain.disabled || destinationChain.disabled ) { return false } // Check if destination token is supported on destination chain const isDestinationTokenSupported = destinationChain.solverCurrencies.some( (currency) => currency.address.toLowerCase() === destinationToken.toLowerCase(), ) if (!isDestinationTokenSupported) { return false } // If we have a client available, try to get a quote to verify the route const client = getClient() if (client) { try { const sender = "0x1111111111111111111111111111111111111111" const quote = await client.actions.getQuote({ chainId: originChainId, toChainId: destinationChainId, amount: amount, currency: originToken, toCurrency: destinationToken, tradeType: "EXACT_OUTPUT", txs: [], user: sender, recipient: sender, }) return quote.steps.length > 0 } catch (error) { logger.console.warn( "[trails-sdk] Quote check failed, falling back to chain/token validation:", error, ) // If quote fails, we still return true if chains and tokens are supported return true } } // If no client available, return true if chains and tokens are supported return true } catch (error) { logger.console.error("[trails-sdk] Error checking route support:", error) return false } } interface RelayStatusResponse { status: string inTxHashes: string[] txHashes: string[] time: number originChainId: number destinationChainId: number } /** * Fetch the status of a relay request by request ID */ export async function fetchRelayRequestStatus( requestId: string, ): Promise<RelayStatusResponse> { try { const response = await fetch( `https://api.relay.link/intents/status/v2?requestId=${requestId}`, ) if (!response.ok) { throw new Error( `Failed to fetch relay status: ${response.status} ${response.statusText}`, ) } const data = await response.json() return data as RelayStatusResponse } catch (error) { logger.console.error( "[trails-sdk] Error fetching relay request status:", error, ) throw error } } /** * Wait for relay destination transaction to complete and return the last transaction hash */ export async function waitForRelayDestinationTx( quoteProviderRequestId: string, ): Promise<string> { const maxWaitTime = 5 * 60 * 1000 // 5 minutes const pollInterval = 5000 // 5 seconds const startTime = Date.now() const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)) while (Date.now() - startTime < maxWaitTime) { try { const status = await fetchRelayRequestStatus(quoteProviderRequestId) if (!status) { throw new Error("No status found") } if ( status.inTxHashes && status.inTxHashes.length > 0 && status.originChainId === status.destinationChainId ) { return status.inTxHashes[status.inTxHashes.length - 1]! } logger.console.log("[trails-sdk] Relay status check:", { requestId: quoteProviderRequestId, status: status.status, txHashesCount: status.txHashes?.length || 0, }) // Check if we have transaction hashes if (status.txHashes && status.txHashes.length > 0) { // Return the last transaction hash in the array const lastTxHash = status.txHashes[status.txHashes.length - 1] if (lastTxHash) { logger.console.log( "[trails-sdk] Relay transaction completed:", lastTxHash, ) return lastTxHash } } // If status is failed or error, throw an error if (status.status === "failed" || status.status === "error") { throw new Error( `Relay transaction failed with status: ${status.status}`, ) } // Wait before next poll await sleep(pollInterval) } catch (error) { // If it's a fetch error (like 404), continue polling as the request might not be indexed yet if ( error instanceof Error && (error.message.includes("Failed to fetch relay status") || error.message.includes("No status found")) ) { logger.console.warn( "[trails-sdk] Relay status not yet available, continuing to poll...", ) await sleep(pollInterval) continue } // For other errors, rethrow throw error } } throw new Error( `Timeout waiting for relay transaction after ${maxWaitTime / 1000} seconds`, ) }