diff --git a/packages/rest-api/README.md b/packages/rest-api/README.md index 3b83d587dd..d5a80719ea 100644 --- a/packages/rest-api/README.md +++ b/packages/rest-api/README.md @@ -7,6 +7,15 @@ To run locally: yarn dev ``` +## Environment Variables + +The REST API supports the following environment variables: + +| Variable | Description | Default Value | +|----------|-------------|---------------| +| RFQ_API_URL | URL for the RFQ API | https://rfq-api.omnirpc.io | +| RFQ_INDEXER_URL | URL for the RFQ Indexer API | https://rfq-indexer.synapseprotocol.com/api | + # Documentation [Swagger Documentation](https://api.synapseprotocol.com/api-docs/) diff --git a/packages/rest-api/src/constants/bridgeMap.ts b/packages/rest-api/src/constants/bridgeMap.ts index a7cc5cafab..1575d12874 100644 --- a/packages/rest-api/src/constants/bridgeMap.ts +++ b/packages/rest-api/src/constants/bridgeMap.ts @@ -943,8 +943,8 @@ export const BRIDGE_MAP = { '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': { decimals: 18, symbol: 'HYPE', - origin: ['Gas.zip'], - destination: ['Gas.zip'], + origin: ['Gas.zip', 'RFQ.ETH', 'RFQ.USDC'], + destination: ['Gas.zip', 'RFQ.ETH', 'RFQ.USDC'], swappable: [], }, }, diff --git a/packages/rest-api/src/tests/destinationTokensRoute.test.ts b/packages/rest-api/src/tests/destinationTokensRoute.test.ts index a221d3841b..3ae5e9b299 100644 --- a/packages/rest-api/src/tests/destinationTokensRoute.test.ts +++ b/packages/rest-api/src/tests/destinationTokensRoute.test.ts @@ -62,7 +62,7 @@ describe('destinatonTokens Route', () => { expect(response.status).toBe(200) expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBe(8) + expect(response.body.length).toBe(9) expect(response.body[0]).toHaveProperty('symbol') expect(response.body[0]).toHaveProperty('address') expect(response.body[0]).toHaveProperty('chainId') diff --git a/packages/rest-api/src/utils/isGatewayRoute.ts b/packages/rest-api/src/utils/isGatewayRoute.ts index 275cd7e071..65dcaf98e2 100644 --- a/packages/rest-api/src/utils/isGatewayRoute.ts +++ b/packages/rest-api/src/utils/isGatewayRoute.ts @@ -1,5 +1,10 @@ import { createProxyMiddleware } from 'http-proxy-middleware' +// Environment variables for RFQ API and Indexer URLs +const RFQ_API_URL = process.env.RFQ_API_URL || 'https://rfq-api.omnirpc.io' +const RFQ_INDEXER_URL = + process.env.RFQ_INDEXER_URL || 'https://rfq-indexer.synapseprotocol.com/api' + export const isRFQIndexerRequest = (route: string): boolean => { return ( route.includes('/conflicting-proofs') || @@ -24,11 +29,11 @@ export const isRFQAPIRequest = (route: string): boolean => { } export const rfqApiProxy = createProxyMiddleware({ - target: 'https://rfq-api.omnirpc.io', + target: RFQ_API_URL, changeOrigin: true, }) export const rfqIndexerProxy = createProxyMiddleware({ - target: 'https://rfq-indexer.synapseprotocol.com/api', + target: RFQ_INDEXER_URL, changeOrigin: true, }) diff --git a/packages/sdk-router/README.md b/packages/sdk-router/README.md index c21cc98460..0bd2c23fec 100644 --- a/packages/sdk-router/README.md +++ b/packages/sdk-router/README.md @@ -6,6 +6,14 @@ This package contains the Synapse Protocol Cross-Chain Swap and Bridging SDK. [See the Docs](https://synapse-3.gitbook.io/synapse-protocol/developers/bridge-sdk) +## Environment Variables + +The SDK Router supports the following environment variables: + +| Variable | Description | Default Value | +| ----------- | ------------------- | -------------------------- | +| RFQ_API_URL | URL for the RFQ API | https://rfq-api.omnirpc.io | + # Synapse SDK The Synapse SDK allows you to interact with [Synapse Protocol](https://synapseprotocol.com/) router contracts deployed on 19 chains. It handles: diff --git a/packages/sdk-router/src/constants/chainIds.ts b/packages/sdk-router/src/constants/chainIds.ts index 110edf4c58..1f9c965dc8 100644 --- a/packages/sdk-router/src/constants/chainIds.ts +++ b/packages/sdk-router/src/constants/chainIds.ts @@ -8,6 +8,7 @@ export enum SupportedChainId { FANTOM = 250, BOBA = 288, WORLDCHAIN = 480, + HYPEREVM = 999, METIS = 1088, MOONBEAM = 1284, MOONRIVER = 1285, @@ -35,6 +36,7 @@ const UNSUPPORTED_BRIDGE_CHAIN_IDS: number[] = [ SupportedChainId.WORLDCHAIN, SupportedChainId.UNICHAIN, SupportedChainId.BERACHAIN, + SupportedChainId.HYPEREVM, ] /** @@ -89,6 +91,7 @@ export const RFQ_SUPPORTED_CHAIN_IDS: number[] = [ SupportedChainId.SCROLL, SupportedChainId.UNICHAIN, SupportedChainId.BERACHAIN, + SupportedChainId.HYPEREVM, ].filter((chainId) => !PAUSED_CHAIN_IDS.includes(chainId)) /** diff --git a/packages/sdk-router/src/constants/medianTime.ts b/packages/sdk-router/src/constants/medianTime.ts index 244908ca14..d9e5cd769a 100644 --- a/packages/sdk-router/src/constants/medianTime.ts +++ b/packages/sdk-router/src/constants/medianTime.ts @@ -57,4 +57,5 @@ export const MEDIAN_TIME_RFQ = { [SupportedChainId.WORLDCHAIN]: 15, [SupportedChainId.UNICHAIN]: 15, [SupportedChainId.BERACHAIN]: 15, + [SupportedChainId.HYPEREVM]: 15, } diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 1cbbc579f7..0d91203436 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -1,28 +1,45 @@ import { getWithTimeout } from '../utils/api' import { logger } from '../utils/logger' +import { marshallTicker } from './ticker' import { FastBridgeQuote, FastBridgeQuoteAPI, unmarshallFastBridgeQuote, } from './quote' -const API_URL = 'https://rfq-api.omnirpc.io' +const DEFAULT_API_URL = 'https://rfq-api.omnirpc.io' +const CUSTOM_API_URL = process.env.RFQ_API_URL const API_TIMEOUT = 2000 /** - * Hits Quoter API /quotes endpoint to get all quotes. + * Get all API endpoints to query. + * Uses both the default API endpoint and custom API endpoint if defined and different. * - * @returns A promise that resolves to the list of quotes. - * Will return an empty list if the request fails or times out. + * @returns Array of API endpoints to query */ -export const getAllQuotes = async (): Promise => { +const getApiEndpoints = (): string[] => { + return CUSTOM_API_URL && CUSTOM_API_URL !== DEFAULT_API_URL + ? [DEFAULT_API_URL, CUSTOM_API_URL] + : [DEFAULT_API_URL] +} + +/** + * Fetch quotes from a specific API endpoint + * + * @param endpoint The API endpoint URL to query + * @returns Array of unmarshalled FastBridgeQuote objects or empty array if request fails + */ +const getQuotesFromEndpoint = async ( + endpoint: string +): Promise => { try { const response = await getWithTimeout( 'RFQ API', - `${API_URL}/quotes`, + `${endpoint}/quotes`, API_TIMEOUT ) if (!response) { + logger.info({ endpoint }, 'No response from endpoint') return [] } // The response is a list of quotes in the FastBridgeQuoteAPI format @@ -32,11 +49,67 @@ export const getAllQuotes = async (): Promise => { try { return unmarshallFastBridgeQuote(quote) } catch (error) { - logger.error({ quote, error }, 'Could not unmarshall quote') + logger.error({ endpoint, quote, error }, 'Could not unmarshall quote') return null } }) .filter((quote): quote is FastBridgeQuote => quote !== null) + } catch (error) { + logger.error({ endpoint, error }, 'Failed to fetch quotes from endpoint') + return [] + } +} + +/** + * Create a unique key for a quote based on relayer address and ticker + * + * @param quote The quote to create a key for + * @returns A unique string key + */ +const createQuoteKey = (quote: FastBridgeQuote): string => { + return `${quote.relayerAddr}-${marshallTicker(quote.ticker)}` +} + +/** + * Merges quotes from multiple arrays, avoiding duplicates based on relayer + ticker. + * + * @param quotesArrays Arrays of quotes to merge + * @returns Merged array of unique quotes + */ +const mergeQuotes = (quotesArrays: FastBridgeQuote[][]): FastBridgeQuote[] => { + // Use a Map to track unique quotes by a string key representing the relayer + ticker key. + const uniqueQuotes = new Map() + + quotesArrays.forEach((quotes) => { + quotes.forEach((quote) => { + const key = createQuoteKey(quote) + // If we haven't seen this ticker by this relayer before, or if this quote is newer, keep it + const existingQuote = uniqueQuotes.get(key) + if (!existingQuote || quote.updatedAt > existingQuote.updatedAt) { + uniqueQuotes.set(key, quote) + } + }) + }) + + return Array.from(uniqueQuotes.values()) +} + +/** + * Hits all available Quoter API /quotes endpoints to get quotes. + * Merges quotes from all endpoints, preferring the most recently updated quotes. + * + * @returns A promise that resolves to the list of quotes. + * Will return an empty list if all requests fail or time out. + */ +export const getAllQuotes = async (): Promise => { + try { + const endpoints = getApiEndpoints() + // Fetch quotes from all endpoints in parallel + const quotesArrays = await Promise.all( + endpoints.map((endpoint) => getQuotesFromEndpoint(endpoint)) + ) + // Merge all quotes, keeping the most recent ones + return mergeQuotes(quotesArrays) } catch (error) { logger.error({ error }, 'Failed to fetch all quotes') return [] diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 8caee554ed..f373906d7e 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -152,11 +152,17 @@ export class FastBridgeRouterSet extends SynapseModuleSet { feeAmount: BigNumber feeConfig: FeeConfig }> { - // Origin Out vs Dest Out is the effective fee + // TODO: do we actually need to return non-zero alues here? + // Origin Out vs Dest Out is the effective fee if amountOut is within 1% of amountIn. + // Otherwise origin and destination tokens are different, so the SDK has no means to determine the effective fee. + const amountIn = bridgeRoute.originQuery.minAmountOut + const amountOut = bridgeRoute.destQuery.minAmountOut + const feeAmount = + amountOut.gte(amountIn.mul(99).div(100)) && amountOut.lte(amountIn) + ? amountIn.sub(amountOut) + : Zero return { - feeAmount: bridgeRoute.originQuery.minAmountOut.sub( - bridgeRoute.destQuery.minAmountOut - ), + feeAmount, feeConfig: { bridgeFee: 0, minFee: BigNumber.from(0), diff --git a/packages/synapse-interface/constants/bridgeMap.ts b/packages/synapse-interface/constants/bridgeMap.ts index a7cc5cafab..1575d12874 100644 --- a/packages/synapse-interface/constants/bridgeMap.ts +++ b/packages/synapse-interface/constants/bridgeMap.ts @@ -943,8 +943,8 @@ export const BRIDGE_MAP = { '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': { decimals: 18, symbol: 'HYPE', - origin: ['Gas.zip'], - destination: ['Gas.zip'], + origin: ['Gas.zip', 'RFQ.ETH', 'RFQ.USDC'], + destination: ['Gas.zip', 'RFQ.ETH', 'RFQ.USDC'], swappable: [], }, }, diff --git a/packages/synapse-interface/scripts/generateMaps.js b/packages/synapse-interface/scripts/generateMaps.js index eae85c45cd..d1a72f0688 100644 --- a/packages/synapse-interface/scripts/generateMaps.js +++ b/packages/synapse-interface/scripts/generateMaps.js @@ -1,5 +1,5 @@ require('dotenv').config() - +// TODO: handle HYPE-ETH, HYPE-USDC pairs from the RFQ API const { ethers } = require('ethers') const { prettyPrintTS } = require('./utils/prettyPrintTs')