diff --git a/.env b/.env index 859d961f7c0..b0e94e40e58 100644 --- a/.env +++ b/.env @@ -61,6 +61,7 @@ VITE_FEATURE_CHAINFLIP_SWAP_DCA=true VITE_FEATURE_COWSWAP=true VITE_FEATURE_THOR_SWAP=true VITE_FEATURE_ZRX_SWAP=true +VITE_FEATURE_FYND_SWAP=false VITE_FEATURE_MAYA_SWAP=true VITE_FEATURE_AVNU_SWAP=true VITE_FEATURE_DYNAMIC_LP_ASSETS=false @@ -310,6 +311,7 @@ VITE_ZERION_BASE_URL=https://api.proxy.shapeshift.com/api/v1/zerion # 0x VITE_ZRX_BASE_URL=https://api.proxy.shapeshift.com/api/v1/zrx/ +VITE_FYND_ETHEREUM_BASE_URL=http://localhost:3000/fynd-api # Bebop VITE_BEBOP_API_KEY=b4a7ffa9-2abb-45ae-8ddd-ec33bc377939 diff --git a/.env.development b/.env.development index 9f5bd818cf2..979d0dc6fca 100644 --- a/.env.development +++ b/.env.development @@ -107,3 +107,7 @@ VITE_PROXY_API_BASE_URL=https://dev-api.proxy.shapeshift.com # Agentic Chat # VITE_AGENTIC_SERVER_BASE_URL=http://localhost:4111 +VITE_FEATURE_FYND_SWAP=true +VITE_FYND_ETHEREUM_BASE_URL=http://localhost:3000/fynd-api +FYND_ETHEREUM_BASE_URL=https://fynd-api.propellerheads.xyz/v1/ethereum +FYND_API_KEY= diff --git a/.env.production b/.env.production index a8162c25ac2..2669e25bb7e 100644 --- a/.env.production +++ b/.env.production @@ -9,3 +9,5 @@ VITE_FEATURE_CELO=false # mixpanel VITE_MIXPANEL_TOKEN=9d304465fc72224aead9e027e7c24356 +VITE_FEATURE_FYND_SWAP=false +VITE_FYND_ETHEREUM_BASE_URL=http://localhost:3001/v1 diff --git a/headers/csps/defi/swappers/Fynd.ts b/headers/csps/defi/swappers/Fynd.ts new file mode 100644 index 00000000000..baf0be0f204 --- /dev/null +++ b/headers/csps/defi/swappers/Fynd.ts @@ -0,0 +1,5 @@ +import type { Csp } from '../../../types' + +export const csp: Csp = { + 'connect-src': ['http://localhost:3001', 'https://fynd-api.propellerheads.xyz'], +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 7213d29df57..79cd14420c4 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -65,6 +65,7 @@ import { csp as bebop } from './defi/swappers/Bebop' import { csp as bobGateway } from './defi/swappers/BobGateway' import { csp as butterSwap } from './defi/swappers/ButterSwap' import { csp as cowSwap } from './defi/swappers/CowSwap' +import { csp as fynd } from './defi/swappers/Fynd' import { csp as nearIntents } from './defi/swappers/NearIntents' import { csp as oneInch } from './defi/swappers/OneInch' import { csp as portals } from './defi/swappers/Portals' @@ -188,6 +189,7 @@ export const csps = [ bobGateway, bebop, cowSwap, + fynd, nearIntents, oneInch, portals, diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts index 7d50d41dfd2..9ca002eb48f 100644 --- a/packages/public-api/src/config.ts +++ b/packages/public-api/src/config.ts @@ -24,6 +24,7 @@ export const getServerConfig = (): SwapperConfig => ({ VITE_COWSWAP_BASE_URL: env.COWSWAP_BASE_URL, VITE_PORTALS_BASE_URL: env.PORTALS_BASE_URL, VITE_ZRX_BASE_URL: env.ZRX_BASE_URL, + VITE_FYND_ETHEREUM_BASE_URL: '', VITE_CHAINFLIP_API_KEY: env.CHAINFLIP_API_KEY, VITE_CHAINFLIP_API_URL: env.CHAINFLIP_API_URL, VITE_FEATURE_CHAINFLIP_SWAP_DCA: env.FEATURE_CHAINFLIP_SWAP_DCA, diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index 49245a018ff..bf5f20258e2 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -10,6 +10,7 @@ import { cetusApi, cetusSwapper } from './swappers/CetusSwapper' import { chainflipApi, chainflipSwapper } from './swappers/ChainflipSwapper' import { cowApi, cowSwapper } from './swappers/CowSwapper' import { debridgeApi, debridgeSwapper } from './swappers/DebridgeSwapper' +import { fyndApi, fyndSwapper } from './swappers/FyndSwapper' import { mayachainApi, mayachainSwapper } from './swappers/MayachainSwapper' import { nearIntentsApi, nearIntentsSwapper } from './swappers/NearIntentsSwapper' import { portalsApi, portalsSwapper } from './swappers/PortalsSwapper' @@ -104,6 +105,10 @@ export const swappers: Record = ...bobGatewaySwapper, ...bobGatewayApi, }, + [SwapperName.Fynd]: { + ...fyndSwapper, + ...fyndApi, + }, [SwapperName.Test]: undefined, } @@ -132,6 +137,7 @@ export const getDefaultSlippageDecimalPercentageForSwapper = ( switch (swapperName) { case SwapperName.Zrx: case SwapperName.Bebop: + case SwapperName.Fynd: case SwapperName.Test: return DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.CowSwap: diff --git a/packages/swapper/src/index.ts b/packages/swapper/src/index.ts index 3ff10f04cbd..9e27225f7e7 100644 --- a/packages/swapper/src/index.ts +++ b/packages/swapper/src/index.ts @@ -15,6 +15,7 @@ export * from './swappers/ChainflipSwapper' export * from './swappers/SunioSwapper' export * from './swappers/CowSwapper' export * from './swappers/DebridgeSwapper' +export * from './swappers/FyndSwapper' export * from './swappers/PortalsSwapper' export * from './swappers/ThorchainSwapper' export * from './swappers/MayachainSwapper' diff --git a/packages/swapper/src/swappers/FyndSwapper/FyndSwapper.ts b/packages/swapper/src/swappers/FyndSwapper/FyndSwapper.ts new file mode 100644 index 00000000000..d4fa3b169ac --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/FyndSwapper.ts @@ -0,0 +1,4 @@ +import type { Swapper } from '../../types' +import { executeEvmTransaction } from '../../utils' + +export const fyndSwapper: Swapper = { executeEvmTransaction } diff --git a/packages/swapper/src/swappers/FyndSwapper/endpoints.ts b/packages/swapper/src/swappers/FyndSwapper/endpoints.ts new file mode 100644 index 00000000000..75a836caf99 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/endpoints.ts @@ -0,0 +1,14 @@ +import type { SwapperApi } from '../../types' +import { checkEvmSwapStatus } from '../../utils' +import { getEvmTransactionFees, getUnsignedEvmTransaction } from '../../utils/evm' +import { getTradeQuote } from './getTradeQuote/getTradeQuote' +import { getTradeRate } from './getTradeRate/getTradeRate' +import type { FyndTradeQuoteInput, FyndTradeRateInput } from './types' + +export const fyndApi: SwapperApi = { + getTradeQuote: (input, deps) => getTradeQuote(input as FyndTradeQuoteInput, deps), + getTradeRate: (input, deps) => getTradeRate(input as FyndTradeRateInput, deps), + getUnsignedEvmTransaction, + getEvmTransactionFees, + checkTradeStatus: checkEvmSwapStatus, +} diff --git a/packages/swapper/src/swappers/FyndSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/FyndSwapper/getTradeQuote/getTradeQuote.ts new file mode 100644 index 00000000000..ffdfe175b98 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/getTradeQuote/getTradeQuote.ts @@ -0,0 +1,89 @@ +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + SingleHopTradeQuoteSteps, + SwapErrorRight, + SwapperDeps, + TradeQuote, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { assertQuoteAddresses, makeSwapErrorRight } from '../../../utils' +import type { FyndTradeQuoteInput } from '../types' +import { fetchFromFynd } from '../utils/fetchFromFynd' +import { getFyndStepData } from '../utils/getFyndStepData' +import { getFyndTradeContext } from '../utils/getFyndTradeContext' +import { assertValidTrade } from '../utils/helpers' + +export const getTradeQuote = async ( + input: FyndTradeQuoteInput, + deps: SwapperDeps, +): Promise> => { + const maybeAddresses = assertQuoteAddresses(input) + if (maybeAddresses.isErr()) return Err(maybeAddresses.unwrapErr()) + const { sendAddress, receiveAddress } = maybeAddresses.unwrap() + + const validation = assertValidTrade(input) + if (validation.isErr()) return Err(validation.unwrapErr()) + + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Fynd) + const maybeFynd = await fetchFromFynd({ + sellAsset: input.sellAsset, + buyAsset: input.buyAsset, + sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + sender: sendAddress, + receiver: receiveAddress, + slippageTolerancePercentageDecimal, + baseUrl: deps.config.VITE_FYND_ETHEREUM_BASE_URL, + quoteOrRate: 'quote', + }) + if (maybeFynd.isErr()) return Err(maybeFynd.unwrapErr()) + const { quote, routerAddress } = maybeFynd.unwrap() + if (!quote.transaction) { + return Err( + makeSwapErrorRight({ + message: 'Fynd returned an unencoded quote', + code: TradeQuoteError.InvalidResponse, + }), + ) + } + + const maybeContext = getFyndTradeContext({ + input, + quote, + routerAddress, + slippageTolerancePercentageDecimal, + }) + if (maybeContext.isErr()) return Err(maybeContext.unwrapErr()) + const { tradeCommon, stepCommon, protocolFees } = maybeContext.unwrap() + + const maybeStepData = await getFyndStepData({ + type: 'quote', + input, + deps, + sellAsset: input.sellAsset, + transaction: quote.transaction, + from: sendAddress, + }) + if (maybeStepData.isErr()) return Err(maybeStepData.unwrapErr()) + const { transactionData, networkFeeCryptoBaseUnit } = maybeStepData.unwrap() + + const tradeQuote: TradeQuote = { + ...tradeCommon, + quoteOrRate: 'quote', + receiveAddress, + steps: [ + { + ...stepCommon, + accountNumber: input.accountNumber, + transactionData, + feeData: { networkFeeCryptoBaseUnit, protocolFees }, + }, + ] as SingleHopTradeQuoteSteps, + } + + return Ok([tradeQuote]) +} diff --git a/packages/swapper/src/swappers/FyndSwapper/getTradeRate/getTradeRate.ts b/packages/swapper/src/swappers/FyndSwapper/getTradeRate/getTradeRate.ts new file mode 100644 index 00000000000..6d4d1fc681c --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/getTradeRate/getTradeRate.ts @@ -0,0 +1,76 @@ +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' + +import { getDefaultSlippageDecimalPercentageForSwapper } from '../../../constants' +import type { + SingleHopTradeRateSteps, + SwapErrorRight, + SwapperDeps, + TradeRate, +} from '../../../types' +import { SwapperName } from '../../../types' +import type { FyndTradeRateInput } from '../types' +import { FYND_RATE_ADDRESS } from '../utils/constants' +import { fetchFromFynd } from '../utils/fetchFromFynd' +import { getFyndStepData } from '../utils/getFyndStepData' +import { getFyndTradeContext } from '../utils/getFyndTradeContext' +import { assertValidTrade } from '../utils/helpers' + +export const getTradeRate = async ( + input: FyndTradeRateInput, + deps: SwapperDeps, +): Promise> => { + const validation = assertValidTrade(input) + if (validation.isErr()) return Err(validation.unwrapErr()) + + const address = input.receiveAddress ?? FYND_RATE_ADDRESS + const slippageTolerancePercentageDecimal = + input.slippageTolerancePercentageDecimal ?? + getDefaultSlippageDecimalPercentageForSwapper(SwapperName.Fynd) + const maybeFynd = await fetchFromFynd({ + sellAsset: input.sellAsset, + buyAsset: input.buyAsset, + sellAmountCryptoBaseUnit: input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + sender: address, + receiver: address, + slippageTolerancePercentageDecimal, + baseUrl: deps.config.VITE_FYND_ETHEREUM_BASE_URL, + quoteOrRate: 'rate', + }) + if (maybeFynd.isErr()) return Err(maybeFynd.unwrapErr()) + const { quote, routerAddress } = maybeFynd.unwrap() + + const maybeContext = getFyndTradeContext({ + input, + quote, + routerAddress, + slippageTolerancePercentageDecimal, + }) + if (maybeContext.isErr()) return Err(maybeContext.unwrapErr()) + const { tradeCommon, stepCommon, protocolFees } = maybeContext.unwrap() + const maybeStepData = await getFyndStepData({ + type: 'rate', + input, + deps, + sellAsset: input.sellAsset, + gasEstimate: quote.gas_estimate, + gasPrice: quote.gas_price, + }) + if (maybeStepData.isErr()) return Err(maybeStepData.unwrapErr()) + const { networkFeeCryptoBaseUnit } = maybeStepData.unwrap() + + const tradeRate: TradeRate = { + ...tradeCommon, + quoteOrRate: 'rate', + receiveAddress: input.receiveAddress, + steps: [ + { + ...stepCommon, + accountNumber: input.accountNumber, + feeData: { networkFeeCryptoBaseUnit, protocolFees }, + }, + ] as SingleHopTradeRateSteps, + } + + return Ok([tradeRate]) +} diff --git a/packages/swapper/src/swappers/FyndSwapper/index.ts b/packages/swapper/src/swappers/FyndSwapper/index.ts new file mode 100644 index 00000000000..82d21865625 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/index.ts @@ -0,0 +1,3 @@ +export { fyndApi } from './endpoints' +export { fyndSwapper } from './FyndSwapper' +export * from './types' diff --git a/packages/swapper/src/swappers/FyndSwapper/types.ts b/packages/swapper/src/swappers/FyndSwapper/types.ts new file mode 100644 index 00000000000..98c8084f4d3 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/types.ts @@ -0,0 +1,60 @@ +import type { Address, Hex } from 'viem' + +import type { GetEvmTradeQuoteInput, GetEvmTradeRateInput } from '../../types' + +export type FyndTradeQuoteInput = GetEvmTradeQuoteInput +export type FyndTradeRateInput = GetEvmTradeRateInput + +export type FyndQuoteStatus = + | 'success' + | 'no_route_found' + | 'insufficient_liquidity' + | 'timeout' + | 'not_ready' + | 'price_check_failed' + +export type FyndTransaction = { + to: Address + value: string + data: Hex + client_fee_signature_offset: number | null +} + +export type FyndFeeBreakdown = { + router_fee: string + client_fee: string + max_slippage: string + min_amount_received: string + swaps_hash: string | null +} + +export type FyndOrderQuote = { + order_id: string + status: FyndQuoteStatus + amount_in: string + amount_out: string + amount_out_net_gas: string + gas_estimate: string + gas_price: string | null + price_impact_bps: number | null + route: { + swaps: { + protocol: string + }[] + } | null + transaction: FyndTransaction | null + fee_breakdown: FyndFeeBreakdown | null +} + +export type FyndQuoteResponse = { + orders: FyndOrderQuote[] + total_gas_estimate: string + solve_time_ms: number +} + +export type FyndInfoResponse = { + chain_id: number + router_address: Address | null + permit2_address: Address | null + version: string +} diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/constants.ts b/packages/swapper/src/swappers/FyndSwapper/utils/constants.ts new file mode 100644 index 00000000000..07a0dd47603 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/constants.ts @@ -0,0 +1,9 @@ +import { KnownChainIds } from '@shapeshiftoss/types' +import type { Address } from 'viem' + +export const FYND_SUPPORTED_CHAIN_IDS = [KnownChainIds.EthereumMainnet] as const +export type FyndSupportedChainId = (typeof FYND_SUPPORTED_CHAIN_IDS)[number] + +export const FYND_NATIVE_ASSET_ADDRESS = '0x0000000000000000000000000000000000000000' as Address +export const FYND_RATE_ADDRESS = '0x0000000000000000000000000000000000000001' as Address +export const FYND_ROUTER_FEE_DIVISOR = '100000' diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/fetchFromFynd.ts b/packages/swapper/src/swappers/FyndSwapper/utils/fetchFromFynd.ts new file mode 100644 index 00000000000..8fc9e1ab06b --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/fetchFromFynd.ts @@ -0,0 +1,82 @@ +import type { Asset } from '@shapeshiftoss/types' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' + +import type { SwapErrorRight } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import type { FyndInfoResponse, FyndOrderQuote, FyndQuoteResponse } from '../types' +import { createFyndService } from './fyndService' +import { convertAssetIdToFyndToken } from './helpers' +import { validateFyndInfoResponse, validateFyndQuoteResponse } from './validation' + +type FetchFyndInput = { + sellAsset: Asset + buyAsset: Asset + sellAmountCryptoBaseUnit: string + sender: string + receiver: string + slippageTolerancePercentageDecimal: string + baseUrl: string + quoteOrRate: 'quote' | 'rate' +} + +const quoteStatusToError = (status: FyndOrderQuote['status']): SwapErrorRight => { + const code = + status === 'no_route_found' || status === 'insufficient_liquidity' + ? TradeQuoteError.NoRouteFound + : TradeQuoteError.QueryFailed + return makeSwapErrorRight({ message: `Fynd quote failed with status ${status}`, code }) +} + +export const fetchFromFynd = async ({ + sellAsset, + buyAsset, + sellAmountCryptoBaseUnit, + sender, + receiver, + slippageTolerancePercentageDecimal, + baseUrl, + quoteOrRate, +}: FetchFyndInput): Promise< + Result<{ quote: FyndOrderQuote; routerAddress: string }, SwapErrorRight> +> => { + const service = createFyndService({ baseUrl }) + const maybeInfo = await service.get('/info') + if (maybeInfo.isErr()) return Err(maybeInfo.unwrapErr()) + + const maybeValidInfo = validateFyndInfoResponse(maybeInfo.unwrap().data) + if (maybeValidInfo.isErr()) return Err(maybeValidInfo.unwrapErr()) + const { router_address: routerAddress } = maybeValidInfo.unwrap() + + const maybeResponse = await service.post('/quote', { + orders: [ + { + token_in: convertAssetIdToFyndToken(sellAsset.assetId), + token_out: convertAssetIdToFyndToken(buyAsset.assetId), + amount: sellAmountCryptoBaseUnit, + side: 'sell', + sender, + receiver, + }, + ], + options: { + timeout_ms: 5_000, + min_responses: 1, + ...(quoteOrRate === 'quote' && { + encoding_options: { + slippage: Number(slippageTolerancePercentageDecimal), + transfer_type: 'transfer_from', + }, + }), + }, + }) + + if (maybeResponse.isErr()) return Err(maybeResponse.unwrapErr()) + const maybeValidResponse = validateFyndQuoteResponse(maybeResponse.unwrap().data, quoteOrRate) + if (maybeValidResponse.isErr()) return Err(maybeValidResponse.unwrapErr()) + const quote = maybeValidResponse.unwrap().orders[0] + if (quote.status !== 'success') return Err(quoteStatusToError(quote.status)) + + return Ok({ quote, routerAddress }) +} diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/fyndService.ts b/packages/swapper/src/swappers/FyndSwapper/utils/fyndService.ts new file mode 100644 index 00000000000..c2d53a54683 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/fyndService.ts @@ -0,0 +1,18 @@ +import type { AxiosRequestConfig } from 'axios' + +import type { MonadicSwapperAxiosService } from '../../../types' +import { createCache, makeSwapperAxiosServiceMonadic } from '../../../utils' + +const axiosConfig: AxiosRequestConfig = { timeout: 10_000 } + +type FyndServiceConfig = { + baseUrl: string +} + +export const createFyndService = ({ baseUrl }: FyndServiceConfig): MonadicSwapperAxiosService => { + const cache = createCache(5_000, ['/quote', '/info'], { + ...axiosConfig, + baseURL: baseUrl, + }) + return makeSwapperAxiosServiceMonadic(cache) +} diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/getFyndStepData.ts b/packages/swapper/src/swappers/FyndSwapper/utils/getFyndStepData.ts new file mode 100644 index 00000000000..3fa592267e4 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/getFyndStepData.ts @@ -0,0 +1,63 @@ +import { fromChainId } from '@shapeshiftoss/caip' +import { bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' + +import type { StepDataArgs, SwapErrorRight, TxBuildData } from '../../../types' +import { makeNetworkFeeEstimationFailedErr } from '../../../utils' +import { getEvmNetworkFeeCryptoBaseUnit } from '../../../utils/evm' +import type { FyndTransaction } from '../types' + +export type GetFyndStepDataArgs = StepDataArgs< + unknown, + { gasEstimate: string; gasPrice: string | null }, + { transaction: FyndTransaction; from: string } +> + +type FyndRateStepData = { networkFeeCryptoBaseUnit: string } +type FyndQuoteStepData = { transactionData: TxBuildData; networkFeeCryptoBaseUnit: string } + +export function getFyndStepData( + args: Extract, +): Promise> +export function getFyndStepData( + args: Extract, +): Promise> +export async function getFyndStepData( + args: GetFyndStepDataArgs, +): Promise> { + if (args.type === 'rate') { + if (args.gasPrice === null) { + return Err( + makeNetworkFeeEstimationFailedErr( + 'getFyndStepData', + new Error('Fynd returned a null gas price'), + ), + ) + } + return Ok({ + networkFeeCryptoBaseUnit: bn(args.gasEstimate).times(args.gasPrice).toFixed(), + }) + } + + const { sellAsset, transaction, from, input, deps } = args + const transactionData: TxBuildData = { + type: 'evm', + chainId: Number(fromChainId(sellAsset.chainId).chainReference), + to: transaction.to, + data: transaction.data, + value: transaction.value, + } + + try { + const networkFeeCryptoBaseUnit = await getEvmNetworkFeeCryptoBaseUnit({ + adapter: deps.assertGetEvmChainAdapter(sellAsset.chainId), + transactionData, + from, + supportsEIP1559: 'supportsEIP1559' in input ? input.supportsEIP1559 : false, + }) + return Ok({ transactionData, networkFeeCryptoBaseUnit }) + } catch (error) { + return Err(makeNetworkFeeEstimationFailedErr('getFyndStepData', error)) + } +} diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/getFyndTradeContext.ts b/packages/swapper/src/swappers/FyndSwapper/utils/getFyndTradeContext.ts new file mode 100644 index 00000000000..b180a8b8e6d --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/getFyndTradeContext.ts @@ -0,0 +1,87 @@ +import { bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Ok } from '@sniptt/monads' +import { v4 as uuid } from 'uuid' + +import type { + QuoteFeeData, + SwapErrorRight, + SwapSource, + TradeCommon, + TradeStepCommon, +} from '../../../types' +import { SwapperName } from '../../../types' +import type { FyndOrderQuote, FyndTradeQuoteInput, FyndTradeRateInput } from '../types' +import { + calculateFyndAmounts, + calculateFyndRate, + calculateFyndRouterFee, + isNativeFyndSell, +} from './helpers' + +type FyndTradeContext = { + tradeCommon: TradeCommon + stepCommon: Omit + protocolFees: QuoteFeeData['protocolFees'] +} + +export const getFyndTradeContext = ({ + input, + quote, + routerAddress, + slippageTolerancePercentageDecimal, +}: { + input: FyndTradeQuoteInput | FyndTradeRateInput + quote: FyndOrderQuote + routerAddress: string + slippageTolerancePercentageDecimal: string +}): Result => { + const { sellAsset, buyAsset, sellAmountIncludingProtocolFeesCryptoBaseUnit } = input + const routerFee = quote.fee_breakdown?.router_fee ?? calculateFyndRouterFee(quote.amount_out) + const clientFee = quote.fee_breakdown?.client_fee ?? '0' + const { buyAmountBeforeFeesCryptoBaseUnit, buyAmountAfterFeesCryptoBaseUnit } = + calculateFyndAmounts({ amountOut: quote.amount_out, routerFee, clientFee }) + const rate = calculateFyndRate({ + sellAmount: quote.amount_in, + buyAmount: buyAmountAfterFeesCryptoBaseUnit, + sellAsset, + buyAsset, + }) + const protocol = quote.route?.swaps[0]?.protocol.replace(/^vm:/, '') + const source: SwapSource = protocol ? `${SwapperName.Fynd} • ${protocol}` : SwapperName.Fynd + const protocolFees: QuoteFeeData['protocolFees'] = bn(routerFee).gt(0) + ? { + [buyAsset.assetId]: { + amountCryptoBaseUnit: routerFee, + asset: buyAsset, + requiresBalance: false, + }, + } + : {} + + return Ok({ + tradeCommon: { + id: uuid(), + rate, + swapperName: SwapperName.Fynd, + affiliateBps: '0', + slippageTolerancePercentageDecimal, + priceImpactPercentageDecimal: + quote.price_impact_bps === null + ? undefined + : bn(quote.price_impact_bps).div(10_000).toFixed(), + }, + stepCommon: { + estimatedExecutionTimeMs: 0, + allowanceContract: isNativeFyndSell(sellAsset.assetId) ? '' : routerAddress, + rate, + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + source, + }, + protocolFees, + }) +} diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/helpers.test.ts b/packages/swapper/src/swappers/FyndSwapper/utils/helpers.test.ts new file mode 100644 index 00000000000..eeef1dc83bd --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/helpers.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import { calculateFyndAmounts, calculateFyndRouterFee } from './helpers' + +describe('Fynd fee calculations', () => { + it('calculates the default 0.1 bps router fee using integer arithmetic', () => { + expect(calculateFyndRouterFee('1000000')).toBe('10') + }) + + it('subtracts router and client fees from the expected output', () => { + expect( + calculateFyndAmounts({ amountOut: '1000000', routerFee: '1010', clientFee: '4000' }), + ).toEqual({ + buyAmountBeforeFeesCryptoBaseUnit: '1000000', + buyAmountAfterFeesCryptoBaseUnit: '994990', + }) + }) +}) diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/helpers.ts b/packages/swapper/src/swappers/FyndSwapper/utils/helpers.ts new file mode 100644 index 00000000000..e4be0a35de6 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/helpers.ts @@ -0,0 +1,97 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { fromAssetId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import { bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import type { Address } from 'viem' +import { getAddress, zeroAddress } from 'viem' + +import type { SwapErrorRight } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' +import type { FyndSupportedChainId } from './constants' +import { + FYND_NATIVE_ASSET_ADDRESS, + FYND_ROUTER_FEE_DIVISOR, + FYND_SUPPORTED_CHAIN_IDS, +} from './constants' + +export const isFyndSupportedChainId = (chainId: ChainId): chainId is FyndSupportedChainId => + FYND_SUPPORTED_CHAIN_IDS.includes(chainId as FyndSupportedChainId) + +export type FyndAmounts = { + buyAmountBeforeFeesCryptoBaseUnit: string + buyAmountAfterFeesCryptoBaseUnit: string +} + +export const convertAssetIdToFyndToken = (assetId: AssetId): Address => { + const { assetNamespace, assetReference } = fromAssetId(assetId) + if (assetNamespace === 'slip44') return FYND_NATIVE_ASSET_ADDRESS + return getAddress(assetReference) +} + +export const assertValidTrade = ({ + sellAsset, + buyAsset, +}: { + sellAsset: Asset + buyAsset: Asset +}): Result => { + if (!isFyndSupportedChainId(sellAsset.chainId) || !isFyndSupportedChainId(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: 'Fynd only supports configured EVM chains', + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + if (sellAsset.chainId !== buyAsset.chainId) { + return Err( + makeSwapErrorRight({ + message: 'Fynd does not support cross-chain trades', + code: TradeQuoteError.CrossChainNotSupported, + }), + ) + } + + return Ok(true) +} + +export const calculateFyndRouterFee = (amountOut: string): string => + bn(amountOut).div(FYND_ROUTER_FEE_DIVISOR).integerValue().toFixed() + +export const calculateFyndAmounts = ({ + amountOut, + routerFee, + clientFee = '0', +}: { + amountOut: string + routerFee: string + clientFee?: string +}): FyndAmounts => ({ + buyAmountBeforeFeesCryptoBaseUnit: amountOut, + buyAmountAfterFeesCryptoBaseUnit: bn(amountOut).minus(routerFee).minus(clientFee).toFixed(), +}) + +export const calculateFyndRate = ({ + sellAmount, + buyAmount, + sellAsset, + buyAsset, +}: { + sellAmount: string + buyAmount: string + sellAsset: Asset + buyAsset: Asset +}): string => + getInputOutputRate({ + sellAmountCryptoBaseUnit: sellAmount, + buyAmountCryptoBaseUnit: buyAmount, + sellAsset, + buyAsset, + }) + +export const isNativeFyndSell = (assetId: AssetId): boolean => + convertAssetIdToFyndToken(assetId) === zeroAddress diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/validation.test.ts b/packages/swapper/src/swappers/FyndSwapper/utils/validation.test.ts new file mode 100644 index 00000000000..a36e6148de7 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/validation.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest' + +import { TradeQuoteError } from '../../../types' +import type { + FyndFeeBreakdown, + FyndInfoResponse, + FyndOrderQuote, + FyndQuoteResponse, + FyndTransaction, +} from '../types' +import { validateFyndInfoResponse, validateFyndQuoteResponse } from './validation' + +type InvalidFyndInfoResponse = Omit, 'router_address'> & { + router_address: unknown +} + +const VALID_TRANSACTION: FyndTransaction = { + to: '0xda892c989d07a18b5dd3f392d949f00df15c5736', + value: '0', + data: '0x1234', + client_fee_signature_offset: null, +} + +const VALID_FEE_BREAKDOWN: FyndFeeBreakdown = { + router_fee: '10', + client_fee: '0', + max_slippage: '100', + min_amount_received: '990', + swaps_hash: null, +} + +const VALID_ORDER: FyndOrderQuote = { + order_id: 'order-id', + status: 'success', + amount_in: '1000', + amount_out: '1000', + amount_out_net_gas: '990', + gas_estimate: '21000', + gas_price: '1000000000', + price_impact_bps: 1, + route: { swaps: [{ protocol: 'ekubo_v3' }] }, + transaction: VALID_TRANSACTION, + fee_breakdown: VALID_FEE_BREAKDOWN, +} + +const VALID_QUOTE_RESPONSE: FyndQuoteResponse = { + orders: [VALID_ORDER], + total_gas_estimate: '21000', + solve_time_ms: 20, +} + +describe('Fynd response validation', () => { + it('accepts valid Ethereum info', () => { + const result = validateFyndInfoResponse({ + chain_id: 1, + router_address: '0xda892c989d07a18b5dd3f392d949f00df15c5736', + permit2_address: null, + version: '0.97.14', + }) + + expect(result.isOk()).toBe(true) + }) + + it.each([ + { chain_id: 10, router_address: VALID_TRANSACTION.to }, + { chain_id: 1, router_address: 'invalid' }, + { chain_id: 1, router_address: null }, + ])('rejects invalid Ethereum info', (invalidInfoResponse: InvalidFyndInfoResponse) => { + const result = validateFyndInfoResponse(invalidInfoResponse) + + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.InvalidResponse) + }) + + it('accepts an encoded quote response', () => { + expect(validateFyndQuoteResponse(VALID_QUOTE_RESPONSE, 'quote').isOk()).toBe(true) + }) + + it('accepts a rate response without encoding data', () => { + const rateResponse: FyndQuoteResponse = { + ...VALID_QUOTE_RESPONSE, + orders: [{ ...VALID_ORDER, transaction: null, fee_breakdown: null }], + } + + expect(validateFyndQuoteResponse(rateResponse, 'rate').isOk()).toBe(true) + }) + + it.each([ + { ...VALID_ORDER, amount_out: '-1' }, + { ...VALID_ORDER, amount_out: '0.5' }, + { ...VALID_ORDER, amount_out: '1e3' }, + { ...VALID_ORDER, gas_price: null }, + { ...VALID_ORDER, transaction: null }, + { ...VALID_ORDER, fee_breakdown: null }, + { + ...VALID_ORDER, + fee_breakdown: { ...VALID_FEE_BREAKDOWN, router_fee: '600', client_fee: '401' }, + }, + ])('rejects an invalid encoded quote', (invalidOrder: FyndOrderQuote) => { + const result = validateFyndQuoteResponse( + { ...VALID_QUOTE_RESPONSE, orders: [invalidOrder] }, + 'quote', + ) + + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.InvalidResponse) + }) +}) diff --git a/packages/swapper/src/swappers/FyndSwapper/utils/validation.ts b/packages/swapper/src/swappers/FyndSwapper/utils/validation.ts new file mode 100644 index 00000000000..7ada629c2c5 --- /dev/null +++ b/packages/swapper/src/swappers/FyndSwapper/utils/validation.ts @@ -0,0 +1,141 @@ +import { bn } from '@shapeshiftoss/utils' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import { isAddress, isHex } from 'viem' + +import type { SwapErrorRight } from '../../../types' +import { TradeQuoteError } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import type { + FyndFeeBreakdown, + FyndInfoResponse, + FyndOrderQuote, + FyndQuoteResponse, +} from '../types' + +const FYND_ETHEREUM_CHAIN_ID = 1 +const FYND_QUOTE_STATUSES = new Set([ + 'success', + 'no_route_found', + 'insufficient_liquidity', + 'timeout', + 'not_ready', + 'price_check_failed', +]) + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null + +const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value) + +const isNonNegativeNumericString = (value: unknown): value is string => + typeof value === 'string' && /^\d+$/.test(value) && bn(value).isFinite() && bn(value).gte(0) + +const makeInvalidResponseError = (message: string): SwapErrorRight => + makeSwapErrorRight({ message, code: TradeQuoteError.InvalidResponse }) + +const isValidTransaction = (value: unknown): boolean => + isRecord(value) && + typeof value.to === 'string' && + isAddress(value.to) && + typeof value.data === 'string' && + isHex(value.data) && + isNonNegativeNumericString(value.value) && + (value.client_fee_signature_offset === null || + (typeof value.client_fee_signature_offset === 'number' && + Number.isInteger(value.client_fee_signature_offset) && + value.client_fee_signature_offset >= 0)) + +const isValidFeeBreakdown = (value: unknown): value is FyndFeeBreakdown => + isRecord(value) && + isNonNegativeNumericString(value.router_fee) && + isNonNegativeNumericString(value.client_fee) && + isNonNegativeNumericString(value.max_slippage) && + isNonNegativeNumericString(value.min_amount_received) && + (value.swaps_hash === null || typeof value.swaps_hash === 'string') + +const isValidRoute = (value: unknown): boolean => + value === null || + (isRecord(value) && + isUnknownArray(value.swaps) && + value.swaps.every( + (swap: unknown) => + isRecord(swap) && typeof swap.protocol === 'string' && swap.protocol.length > 0, + )) + +const isValidOrder = (value: unknown, quoteOrRate: 'quote' | 'rate'): value is FyndOrderQuote => { + if (!isRecord(value)) return false + if ( + typeof value.status !== 'string' || + !FYND_QUOTE_STATUSES.has(value.status as FyndOrderQuote['status']) + ) { + return false + } + if ( + typeof value.order_id !== 'string' || + !isNonNegativeNumericString(value.amount_in) || + !isNonNegativeNumericString(value.amount_out) || + !isNonNegativeNumericString(value.amount_out_net_gas) || + !isNonNegativeNumericString(value.gas_estimate) || + !isNonNegativeNumericString(value.gas_price) + ) { + return false + } + if ( + !isValidRoute(value.route) || + (value.price_impact_bps !== null && + (typeof value.price_impact_bps !== 'number' || + !Number.isFinite(value.price_impact_bps) || + value.price_impact_bps < 0)) + ) { + return false + } + if (quoteOrRate === 'quote' && value.status === 'success') { + if (!isValidTransaction(value.transaction) || !isValidFeeBreakdown(value.fee_breakdown)) { + return false + } + + const totalFees = bn(value.fee_breakdown.router_fee).plus(value.fee_breakdown.client_fee) + return totalFees.lte(value.amount_out) + } + return true +} + +export const validateFyndInfoResponse = ( + value: unknown, +): Result => { + if ( + !isRecord(value) || + value.chain_id !== FYND_ETHEREUM_CHAIN_ID || + typeof value.router_address !== 'string' || + !isAddress(value.router_address) || + (value.permit2_address !== null && + (typeof value.permit2_address !== 'string' || !isAddress(value.permit2_address))) || + typeof value.version !== 'string' + ) { + return Err(makeInvalidResponseError('Fynd returned invalid Ethereum router information')) + } + return Ok(value as FyndInfoResponse & { router_address: `0x${string}` }) +} + +export const validateFyndQuoteResponse = ( + value: unknown, + quoteOrRate: 'quote' | 'rate', +): Result< + FyndQuoteResponse & { orders: [FyndOrderQuote, ...FyndOrderQuote[]] }, + SwapErrorRight +> => { + if ( + !isRecord(value) || + !isUnknownArray(value.orders) || + value.orders.length === 0 || + !value.orders.every((order: unknown) => isValidOrder(order, quoteOrRate)) || + typeof value.solve_time_ms !== 'number' || + !Number.isFinite(value.solve_time_ms) || + value.solve_time_ms < 0 || + !isNonNegativeNumericString(value.total_gas_estimate) + ) { + return Err(makeInvalidResponseError('Fynd returned an invalid quote response')) + } + return Ok(value as FyndQuoteResponse & { orders: [FyndOrderQuote, ...FyndOrderQuote[]] }) +} diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 17e0a5a08b7..a18ca07388a 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -81,6 +81,7 @@ export type SwapperConfig = { VITE_COWSWAP_BASE_URL: string VITE_PORTALS_BASE_URL: string VITE_ZRX_BASE_URL: string + VITE_FYND_ETHEREUM_BASE_URL: string VITE_CHAINFLIP_API_KEY: string VITE_CHAINFLIP_API_URL: string VITE_FEATURE_CHAINFLIP_SWAP_DCA: boolean @@ -114,6 +115,7 @@ export enum SwapperName { Across = 'Across', Debridge = 'deBridge', BobGateway = 'BOB Gateway', + Fynd = 'Fynd', } export type SwapSource = SwapperName | `${SwapperName} • ${string}` diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx index ef1a1027435..46b3aa2ff92 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx @@ -13,6 +13,7 @@ import CetusIcon from './cetus-icon.jpg' import ChainflipIcon from './chainflip-icon.png' import CowIcon from './cow-icon.png' import DebridgeIcon from './debridge-icon.svg' +import FyndIcon from './fynd-icon.svg' import MayachainIcon from './maya_logo.png' import NearIntentsIcon from './near-intents-icon.png' import PortalsIcon from './portals-icon.png' @@ -67,6 +68,8 @@ export const SwapperIcon = ({ return AcrossIcon case SwapperName.Debridge: return DebridgeIcon + case SwapperName.Fynd: + return FyndIcon case SwapperName.BobGateway: return BobGatewayIcon case SwapperName.Test: diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/fynd-icon.svg b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/fynd-icon.svg new file mode 100644 index 00000000000..5c65fe21252 --- /dev/null +++ b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/fynd-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/config.ts b/src/config.ts index 969c282f496..ee99b33aa0a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -152,6 +152,7 @@ const validators = { VITE_FEATURE_MAYACHAIN: bool({ default: false }), VITE_FEATURE_ZCASH: bool({ default: false }), VITE_FEATURE_ZRX_SWAP: bool({ default: false }), + VITE_FEATURE_FYND_SWAP: bool({ default: false }), VITE_FEATURE_THOR_SWAP: bool({ default: false }), VITE_FEATURE_CUSTOM_TOKEN_IMPORT: bool({ default: false }), VITE_FEATURE_QUICK_BUY: bool({ default: false }), @@ -241,6 +242,7 @@ const validators = { VITE_FEATURE_FOX_PAGE_GOVERNANCE: bool({ default: false }), VITE_FEATURE_LIMIT_ORDERS: bool({ default: false }), VITE_ZRX_BASE_URL: url(), + VITE_FYND_ETHEREUM_BASE_URL: url({ default: 'http://localhost:3001/v1' }), VITE_FEATURE_CHAINFLIP_SWAP: bool({ default: false }), VITE_FEATURE_CHAINFLIP_SWAP_DCA: bool({ default: false }), VITE_FEATURE_CHAINFLIP_LENDING: bool({ default: false }), diff --git a/src/state/helpers.ts b/src/state/helpers.ts index 06eb479c96d..4d093bb4b01 100644 --- a/src/state/helpers.ts +++ b/src/state/helpers.ts @@ -22,6 +22,7 @@ export const isCrossAccountTradeSupported = (swapperName: SwapperName) => { case SwapperName.Across: case SwapperName.Debridge: case SwapperName.BobGateway: + case SwapperName.Fynd: return true case SwapperName.Zrx: case SwapperName.CowSwap: @@ -43,6 +44,7 @@ export const getEnabledSwappers = ( PortalsSwap, ThorSwap, ZrxSwap, + FyndSwap, ArbitrumBridge, Cowswap, RelaySwapper, @@ -77,6 +79,8 @@ export const getEnabledSwappers = ( !isGridPlusUtxoSell, [SwapperName.Zrx]: ZrxSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Zrx)), + [SwapperName.Fynd]: + FyndSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Fynd)), [SwapperName.CowSwap]: Cowswap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.CowSwap)), [SwapperName.ArbitrumBridge]: diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 98578e786ba..64678ebea63 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -66,6 +66,7 @@ export type FeatureFlags = { WalletConnectToDappsV2: boolean Cowswap: boolean ZrxSwap: boolean + FyndSwap: boolean Mixpanel: boolean DynamicLpAssets: boolean ReadOnlyAssets: boolean @@ -234,6 +235,7 @@ const initialState: Preferences = { WalletConnectToDapps: getConfig().VITE_FEATURE_WALLET_CONNECT_TO_DAPPS, Cowswap: getConfig().VITE_FEATURE_COWSWAP, ZrxSwap: getConfig().VITE_FEATURE_ZRX_SWAP, + FyndSwap: getConfig().VITE_FEATURE_FYND_SWAP, Mixpanel: getConfig().VITE_FEATURE_MIXPANEL, DynamicLpAssets: getConfig().VITE_FEATURE_DYNAMIC_LP_ASSETS, ReadOnlyAssets: getConfig().VITE_FEATURE_READ_ONLY_ASSETS, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 193d7b80e7b..8167d261197 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -134,6 +134,7 @@ export const mockStore: ReduxState = { Soneium: false, Zcash: false, ZrxSwap: false, + FyndSwap: false, ThorSwap: false, Cowswap: false, WalletConnectToDapps: false, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index dc59e8ac170..5469ba8fd79 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -51,6 +51,7 @@ interface ImportMetaEnv { readonly VITE_FEATURE_COWSWAP: string readonly VITE_FEATURE_THOR_SWAP: string readonly VITE_FEATURE_ZRX_SWAP: string + readonly VITE_FEATURE_FYND_SWAP: string readonly VITE_FEATURE_SWAPPER_RELAY: string readonly VITE_FEATURE_MAYA_SWAP: string readonly VITE_FEATURE_BUTTERSWAP: string @@ -111,6 +112,7 @@ interface ImportMetaEnv { readonly VITE_SENTRY_DSN_URL: string readonly VITE_ZERION_BASE_URL: string readonly VITE_ZRX_BASE_URL: string + readonly VITE_FYND_ETHEREUM_BASE_URL: string readonly VITE_CHAINFLIP_API_KEY: string readonly VITE_CHAINFLIP_API_URL: string readonly VITE_CHAINFLIP_RPC_URL: string diff --git a/vite.config.mts b/vite.config.mts index 9d1dc85651c..8299425e7eb 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -247,6 +247,12 @@ export default defineConfig(({ mode }) => { host: '0.0.0.0', allowedHosts: true, proxy: { + '/fynd-api': { + target: env.FYND_ETHEREUM_BASE_URL || 'http://localhost:3001/v1', + changeOrigin: true, + rewrite: path => path.replace(/^\/fynd-api/, ''), + ...(env.FYND_API_KEY && { headers: { Authorization: env.FYND_API_KEY } }), + }, '/user-api': { target: env.USER_API_URL || 'http://localhost:3002', changeOrigin: true,