0xtrails
Version:
SDK for Trails
700 lines (633 loc) • 16.9 kB
text/typescript
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`,
)
}