import { Value as core_Value } from 'ox' import * as z from 'zod/mini' import type * as FundingChain from '../Chain.js' import type * as FundingToken from '../Token.js' import * as FundingProvider from '../Provider.js' const defaultBaseUrl = 'https://api.rhino.fi/bridge' const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init) const chainIds = { 'eip155:4217': 'TEMPO', 'eip155:8453': 'BASE', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': 'SOLANA', 'tron:0x2b6653dc': 'TRON', } as const satisfies Record const tokenIds = { usdce: 'USDC', usdt0: 'USDT', } as const satisfies Record namespace schema { export const Quote = z.object({ _tag: z.literal('bridge'), chainIn: z.string(), chainOut: z.string(), estimatedDuration: z.optional(z.nullable(z.number().check(z.nonnegative()))), payAmount: z.string().check(z.regex(/^\d+(\.\d+)?$/)), receiveAmount: z.string().check(z.regex(/^\d+(\.\d+)?$/)), token: z.string(), }) } /** Creates a Rhino funding provider backed by its public bridge-and-swap quote endpoint. */ export function rhino(options: rhino.Options = {}) { const endpoint = new URL(options.baseUrl ?? defaultBaseUrl) endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/quote/bridge-swap/public` const fetch = options.fetch ?? defaultFetch const now = options.now ?? (() => new Date()) return FundingProvider.from({ id: 'rhino', name: 'Rhino', async getQuote({ cache, candidate, route }, signal) { const chainIn = chainId(route.source.chain) const chainOut = chainId(route.destination.chain) const tokenOut = tokenId(route.destination) const url = new URL(endpoint) url.search = new URLSearchParams({ amount: candidate.sourceAmount.formatted, amountNative: '0', chainIn, chainOut, mode: 'pay', tokenIn: candidate.sourceToken.symbol, tokenOut, }).toString() try { const quote = await FundingProvider.requestJson(url, { fetch, headers: cache.bypass ? { 'cache-control': 'no-cache' } : undefined, signal, }) const parsed = schema.Quote.safeParse(quote) if (!parsed.success) throw new FundingProvider.ProviderPayloadError() const response = parsed.data const inputAmount = parseAmount(response.payAmount, candidate.sourceToken.decimals) const destinationAmount = parseAmount( response.receiveAmount, candidate.destinationToken.decimals, ) if ( response.chainIn !== chainIn || response.chainOut !== chainOut || response.token !== tokenOut || inputAmount !== candidate.sourceAmount.amount || destinationAmount === undefined || BigInt(destinationAmount) <= 0n ) throw new FundingProvider.ProviderPayloadError() return { destinationAmount, quality: { ...(response.estimatedDuration === undefined || response.estimatedDuration === null ? {} : { estimatedSeconds: Math.ceil(response.estimatedDuration / 1_000) }), liquiditySource: 'providerQuote', sourceDetail: 'rhino:bridge-swap-public', tier: 'liquid', }, sampledAt: now().toISOString(), status: 'available', } } catch (cause) { const unavailable = FundingProvider.unavailable(cause, ['NoRouteFoundError']) if (!unavailable) throw cause return FundingProvider.unavailableResult({ detail: 'rhino:bridge-swap-public', message: unavailable, now: now(), source: 'providerQuote', }) } }, }) } export declare namespace rhino { /** Rhino provider options. */ type Options = { /** Override for tests or Rhino test environments. */ baseUrl?: string | undefined /** Fetch implementation override for tests. */ fetch?: FundingProvider.Fetch | undefined /** Clock override for deterministic tests. */ now?: (() => Date) | undefined } } function parseAmount(value: string, decimals: number) { const fraction = value.split('.')[1]?.replace(/0+$/, '') ?? '' if (fraction.length > decimals) return undefined return core_Value.from(value, decimals).toString() } function chainId(chain: FundingChain.Chain) { const id = chainIds[chain.id as keyof typeof chainIds] if (!id) throw new FundingProvider.ProviderConfigurationError() return id } function tokenId(token: FundingToken.Resolved) { const id = tokenIds[token.slug as keyof typeof tokenIds] if (!id) throw new FundingProvider.ProviderConfigurationError() return id }