Skip to content

Commit 540af2e

Browse files
Merge pull request #18 from reserve-protocol/feature/velora
Feature: add Velora
2 parents 0319734 + 89c0496 commit 540af2e

10 files changed

Lines changed: 145 additions & 73 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## [1.5.3] - 2025-11-16
2+
3+
### Changed
4+
5+
- Odos was replaced by Velora for BSC
6+
- Refactored minimum input value logic for DTFs:
7+
- When input value < $1000 for specific DTFs: tries Odos only first, falls back to Zapper if Odos fails
8+
- When input value >= $1000: tries both sources in parallel as before
9+
- Removed error throwing when below minimum value for better UX
10+
111
## [1.5.2] - 2025-11-11
212

313
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reserve-protocol/react-zapper",
3-
"version": "1.5.2",
3+
"version": "1.5.3",
44
"type": "module",
55
"description": "React component for DTF minting with zap functionality",
66
"main": "dist/index.cjs.js",

src/components/icons/velora.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const VeloraIcon = ({
2+
className = '',
3+
size = 16,
4+
}: {
5+
className?: string
6+
size?: number
7+
}) => (
8+
<svg
9+
width={size}
10+
height={size}
11+
viewBox="0 0 676 676"
12+
fill="none"
13+
xmlns="http://www.w3.org/2000/svg"
14+
className={className}
15+
>
16+
<rect width="676" height="676" rx="338" fill="white" fillOpacity={0.9} />
17+
18+
<g transform="translate(-101.4 -101.4) scale(1.3)">
19+
<path
20+
fillRule="evenodd"
21+
clipRule="evenodd"
22+
d="M416.273 197.612C454.831 160.588 541.36 158.849 495.825 253.037C495.825 253.037 504.378 262.678 509.753 270.536C511.188 273.746 509.226 276.662 507.095 279.829C504.651 283.462 501.984 287.426 503.964 292.541C506.919 300.179 511.571 307.655 516.915 316.244C523.649 327.067 531.483 339.656 538.406 356.562C550.823 386.882 533.344 433.783 516.559 461.003C487.808 505.558 440.198 487.811 401.14 468.609C395.62 465.895 388.938 468.375 386.618 474.072L365.824 525.11C362.526 533.204 354.007 537.939 345.603 535.537C286.398 518.614 185.769 458.913 133.843 331.158C130.02 321.752 135.049 311.13 144.042 306.417C154.032 301.182 166.284 293.347 174.26 283.327C174.26 283.327 126.387 205.976 179.836 178.971C213.745 164.088 248.092 174.764 265.954 183.985C267.964 184.9 269.732 185.728 271.317 186.47C283.286 192.074 284.852 192.808 301.55 189.29C342.849 179.873 385.561 184.769 416.273 197.612ZM427.348 297.469C433.513 314.486 441.194 334.131 447.951 346.81C451.718 353.879 458.995 357.92 466.952 358.84C501.494 362.834 512.323 359.627 519.129 356.524C522.202 355.123 523.87 351.655 521.991 348.849C521.991 348.849 495.191 304.227 481.942 286.832C481.621 286.362 481.297 285.891 480.973 285.421C473.976 275.252 467.081 265.23 489.252 265.628C424.963 199.107 366.093 188.441 297.663 203.452C284.356 206.067 277.092 204.149 263.321 197.234C248.619 189.796 227.699 181.371 200.679 186.135C219.905 198.693 235.837 218.439 246.974 235.666C260.951 257.287 284.649 273.167 310.323 271.258L377.415 266.268C398.35 264.711 420.197 277.732 427.348 297.469ZM430.323 202.822C438.543 195.412 464.928 182.032 484.998 189.097C474.018 189.612 466.579 194.513 459.852 198.945C450.949 204.81 443.293 209.854 430.323 202.822ZM175.473 335.075C171.464 321.441 161.359 307.576 144.397 321.401C179.048 411.983 240.43 460.273 240.43 460.273C207.747 425.972 186.456 367.667 175.473 335.075ZM415.454 411.242C367.602 413.153 335.816 412.161 318.904 397.32C444.206 343.012 499.73 378.487 499.73 410.42C499.781 441.032 474.909 450.043 441.026 448.241C407.142 446.439 374.252 433.811 358.234 422.966C358.234 422.966 393.697 423.365 415.454 411.242ZM530.402 377.719C523.636 382.452 515.98 394.484 513.637 409.246C503.463 469.33 535.235 409.89 530.402 377.719ZM506.553 452.29C485.735 492.053 433.301 468.342 406.995 454.367C444.726 463.729 477.571 460.651 499.88 451.03C502.16 450.046 504.83 450.501 506.553 452.29ZM285.709 285.617L336.582 282.559C341.695 282.251 346.349 285.591 349.927 289.255C356.938 296.435 375.073 304.166 407.077 302.93C411.397 302.764 415.58 304.911 417.679 308.69C431.403 340.304 421.019 352.069 356.255 369.833C236.754 402.611 187.679 292.908 285.709 285.617Z"
23+
fill="#110F18"
24+
/>
25+
</g>
26+
</svg>
27+
)
28+
29+
export default VeloraIcon

src/components/zap-mint/buy/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const Buy = ({ mode = 'modal' }: BuyProps) => {
5959
const setCurrentTab = useSetAtom(zapperCurrentTabAtom)
6060
const setOpen = useSetAtom(openZapMintModalAtom)
6161
const selectedTokenPrice = usePrice(chainId, selectedToken.address)
62-
const inputPrice = (selectedTokenPrice || 0) * Number(inputAmount)
62+
const inputValue = (selectedTokenPrice || 0) * Number(inputAmount)
6363
const onMax = () => setInputAmount(selectedTokenBalance?.balance || '0')
6464

6565
const handleTokenSelect = (token: Token) => {
@@ -80,7 +80,7 @@ const Buy = ({ mode = 'modal' }: BuyProps) => {
8080
forceMint,
8181
dtfTicker: indexDTF?.token.symbol || '',
8282
type: 'buy',
83-
inputPrice,
83+
inputValue,
8484
})
8585

8686
const zapperErrorMessage = isFetching
@@ -159,7 +159,7 @@ const Buy = ({ mode = 'modal' }: BuyProps) => {
159159
<div className="flex flex-col gap-2 h-full">
160160
<Swap
161161
from={{
162-
price: `$${formatCurrency(priceFrom ?? inputPrice)}`,
162+
price: `$${formatCurrency(priceFrom ?? inputValue)}`,
163163
address: selectedToken.address,
164164
symbol: selectedToken.symbol,
165165
balance: `${formatCurrency(

src/components/zap-mint/sell/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const Sell = ({ mode = 'modal' }: SellProps) => {
6262
const setZapFetching = useSetAtom(zapFetchingAtom)
6363
const setCurrentTab = useSetAtom(zapperCurrentTabAtom)
6464
const setOpen = useSetAtom(openZapMintModalAtom)
65-
const inputPrice = (indexDTFPrice || 0) * Number(inputAmount)
65+
const inputValue = (indexDTFPrice || 0) * Number(inputAmount)
6666
const onMax = () => setInputAmount(indxDTFParsedBalance)
6767

6868
const handleTokenSelect = (token: Token) => {
@@ -81,7 +81,7 @@ const Sell = ({ mode = 'modal' }: SellProps) => {
8181
forceMint,
8282
dtfTicker: indexDTF?.token.symbol || '',
8383
type: 'sell',
84-
inputPrice,
84+
inputValue,
8585
})
8686

8787
const zapperErrorMessage = isFetching
@@ -160,7 +160,7 @@ const Sell = ({ mode = 'modal' }: SellProps) => {
160160
<div className="flex flex-col gap-2 h-full">
161161
<Swap
162162
from={{
163-
price: `$${formatCurrency(priceFrom ?? inputPrice)}`,
163+
price: `$${formatCurrency(priceFrom ?? inputValue)}`,
164164
address: indexDTF.id,
165165
symbol: indexDTF.token.symbol,
166166
balance: `${formatCurrency(Number(indxDTFParsedBalance))}`,

src/components/zap-mint/zap-details.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { SwapDetails } from '../ui/swap'
2-
import { indexDTFAtom } from '../../state/atoms'
1+
import { ChainId } from '@/utils/chains'
2+
import Decimal from 'decimal.js-light'
3+
import { useAtomValue } from 'jotai'
4+
import { Zap } from 'lucide-react'
5+
import { formatUnits } from 'viem'
6+
import { chainIdAtom, indexDTFAtom } from '../../state/atoms'
7+
import { ZapResult } from '../../types/api'
38
import {
49
formatCurrency,
510
formatPercentage,
611
formatTokenAmount,
712
} from '../../utils'
8-
import { ZapResult } from '../../types/api'
9-
import Decimal from 'decimal.js-light'
10-
import { useAtomValue } from 'jotai'
11-
import { formatUnits } from 'viem'
12-
import { selectedTokenOrDefaultAtom } from './atom'
13-
import Help from '../ui/help'
14-
import { Zap } from 'lucide-react'
1513
import OdosIcon from '../icons/odos'
14+
import VeloraIcon from '../icons/velora'
15+
import Help from '../ui/help'
16+
import { SwapDetails } from '../ui/swap'
17+
import { selectedTokenOrDefaultAtom } from './atom'
1618

1719
export const ZapPriceImpact = ({
1820
data,
@@ -50,6 +52,7 @@ const ZapDetails = ({
5052
data: ZapResult
5153
source?: 'zap' | 'odos'
5254
}) => {
55+
const chainId = useAtomValue(chainIdAtom)
5356
const indexDTF = useAtomValue(indexDTFAtom)
5457
const selectedToken = useAtomValue(selectedTokenOrDefaultAtom)
5558
const dtfAsTokenIn =
@@ -111,6 +114,11 @@ const ZapDetails = ({
111114
<Zap size={14} />
112115
<span>Zap</span>
113116
</>
117+
) : chainId === ChainId.BSC ? (
118+
<>
119+
<VeloraIcon size={18} />
120+
<span>Velora</span>
121+
</>
114122
) : (
115123
<>
116124
<OdosIcon size={14} />

src/components/zap-mint/zap-error-msg.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const ReportButton = ({ error }: { error?: string }) => {
5757
const indexDTFPrice = useAtomValue(indexDTFPriceAtom)
5858

5959
const tokenInPrice = operation === 'buy' ? selectedTokenPrice : indexDTFPrice
60-
const inputPrice = (tokenInPrice || 0) * Number(amount)
60+
const inputValue = (tokenInPrice || 0) * Number(amount)
6161

6262
const handleReport = async () => {
6363
if (hasReported || !error || !sessionId || !quoteId || !retryId) return
@@ -79,7 +79,7 @@ const ReportButton = ({ error }: { error?: string }) => {
7979
symbol: tokenOut?.symbol || '',
8080
},
8181
amount: formatToSignificantDigits(Number(amount) || 0),
82-
value: formatCurrency(inputPrice || 0, 0),
82+
value: formatCurrency(inputValue || 0, 0),
8383
}
8484

8585
const response = await fetch(zapper.report(apiUrl), {

src/components/zap-mint/zap-settings.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1+
import { ChainId } from '@/utils/chains'
12
import { broom } from '@lucide/lab'
2-
import { useAtom } from 'jotai'
3-
import { Anvil, Icon, Zap, Route } from 'lucide-react'
3+
import { useAtom, useAtomValue } from 'jotai'
4+
import { Anvil, Icon, Route, Zap } from 'lucide-react'
5+
import {
6+
chainIdAtom,
7+
quoteSourceAtom,
8+
type QuoteSource,
9+
} from '../../state/atoms'
10+
import OdosIcon from '../icons/odos'
11+
import VeloraIcon from '../icons/velora'
412
import { Checkbox } from '../ui/checkbox'
513
import Help from '../ui/help'
6-
import { SlippageSelector } from '../ui/swap'
7-
import { forceMintAtom, slippageAtom } from './atom'
8-
import { quoteSourceAtom, type QuoteSource } from '../../state/atoms'
914
import {
1015
Select,
1116
SelectContent,
1217
SelectItem,
1318
SelectTrigger,
1419
SelectValue,
1520
} from '../ui/select'
16-
import OdosIcon from '../icons/odos'
21+
import { SlippageSelector } from '../ui/swap'
22+
import { forceMintAtom, slippageAtom } from './atom'
1723

1824
const ZapSettingsRowTitle = ({
1925
title,
@@ -29,6 +35,7 @@ const ZapSettingsRowTitle = ({
2935
)
3036

3137
const ZapSettings = () => {
38+
const chainId = useAtomValue(chainIdAtom)
3239
const [slippage, setSlippage] = useAtom(slippageAtom)
3340
const [forceMint, setForceMint] = useAtom(forceMintAtom)
3441
const [quoteSource, setQuoteSource] = useAtom(quoteSourceAtom)
@@ -71,10 +78,17 @@ const ZapSettings = () => {
7178
</div>
7279
</SelectItem>
7380
<SelectItem value="odos">
74-
<div className="flex items-center gap-2">
75-
<OdosIcon size={14} />
76-
<span>Odos</span>
77-
</div>
81+
{chainId === ChainId.BSC ? (
82+
<div className="flex items-center gap-2">
83+
<VeloraIcon size={16} />
84+
<span>Velora</span>
85+
</div>
86+
) : (
87+
<div className="flex items-center gap-2">
88+
<OdosIcon size={14} />
89+
<span>Odos</span>
90+
</div>
91+
)}
7892
</SelectItem>
7993
</SelectContent>
8094
</Select>

src/hooks/useZapSwapQuery.ts

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,12 @@ import {
3636
trackSubmitButtonReady,
3737
} from '../utils/tracking'
3838
import useDebounce from './useDebounce'
39-
import { formatCurrency } from '@/utils'
4039
import { ChainId } from '@/utils/chains'
4140

4241
const DUST_REFRESH_THRESHOLD = 0.025
4342
const PRICE_IMPACT_THRESHOLD = 2
44-
const MIN_INPUT_PRICE_FOR_ZAP = 1000
45-
const DTFS_WITH_MIN_INPUT_PRICE_FOR_ZAP = {
43+
const MIN_INPUT_VALUE_FOR_ZAP = 1000
44+
const DTFS_WITH_MIN_INPUT_VALUE_FOR_ZAP = {
4645
[ChainId.BSC]: ['0x2f8a339b5889ffac4c5a956787cda593b3c36867'].map((address) =>
4746
address.toLowerCase()
4847
),
@@ -57,7 +56,7 @@ const useZapSwapQuery = ({
5756
forceMint,
5857
dtfTicker,
5958
type,
60-
inputPrice,
59+
inputValue,
6160
}: {
6261
tokenIn?: Address
6362
tokenOut?: Address
@@ -67,7 +66,7 @@ const useZapSwapQuery = ({
6766
forceMint: boolean
6867
dtfTicker: string
6968
type: 'buy' | 'sell'
70-
inputPrice: number
69+
inputValue: number
7170
}) => {
7271
const api = useAtomValue(apiUrlAtom)
7372
const chainId = useAtomValue(chainIdAtom)
@@ -195,20 +194,6 @@ const useZapSwapQuery = ({
195194
let priceImpactAttempt = 0
196195
let lastData: ZapResponse
197196

198-
if (
199-
inputPrice < MIN_INPUT_PRICE_FOR_ZAP &&
200-
DTFS_WITH_MIN_INPUT_PRICE_FOR_ZAP[chainId]?.includes(
201-
dtf?.id?.toLowerCase() ?? ''
202-
)
203-
) {
204-
throw new Error(
205-
`Minimum input price for Zap is $${formatCurrency(
206-
MIN_INPUT_PRICE_FOR_ZAP,
207-
0
208-
)}`
209-
)
210-
}
211-
212197
// eslint-disable-next-line no-constant-condition
213198
while (true) {
214199
// currently no retries for zap
@@ -436,8 +421,19 @@ const useZapSwapQuery = ({
436421
return zapQuote
437422
}
438423

424+
const shouldSkipZapper =
425+
DTFS_WITH_MIN_INPUT_VALUE_FOR_ZAP[chainId]?.includes(
426+
dtf?.id?.toLowerCase() ?? ''
427+
) && inputValue < MIN_INPUT_VALUE_FOR_ZAP
428+
439429
return useQuery({
440-
queryKey: ['zapDeploy', zapEndpoint, odosEndpoint, quoteSource],
430+
queryKey: [
431+
'zapDeploy',
432+
zapEndpoint,
433+
odosEndpoint,
434+
quoteSource,
435+
shouldSkipZapper,
436+
],
441437
queryFn: async (): Promise<ZapResponse & { source: 'zap' | 'odos' }> => {
442438
if (!tokenIn || !tokenOut) {
443439
throw new Error('Invalid tokenIn, tokenOut')
@@ -466,31 +462,42 @@ const useZapSwapQuery = ({
466462
} else if (quoteSource === 'odos') {
467463
result = await fetchOdosQuote(newQuoteId, newRetryId)
468464
} else {
469-
// 'best' - fetch both in parallel and select the best
470-
const results = await Promise.allSettled([
471-
fetchZapQuote(newQuoteId, newRetryId),
472-
fetchOdosQuote(newQuoteId, newRetryId),
473-
])
474-
475-
const zapResult =
476-
results[0].status === 'fulfilled' ? results[0].value : undefined
477-
const odosResult =
478-
results[1].status === 'fulfilled' ? results[1].value : undefined
479-
480-
const bestQuote = selectBestQuote(zapResult, odosResult)
481-
482-
if (!bestQuote) {
483-
// Both failed, throw the first error
484-
if (results[0].status === 'rejected') {
485-
throw results[0].reason
465+
// 'best' - handle minimum value logic
466+
if (shouldSkipZapper) {
467+
// Below minimum for specific DTFs - try Odos first, fallback to Zapper if it fails
468+
try {
469+
result = await fetchOdosQuote(newQuoteId, newRetryId)
470+
} catch {
471+
// Odos failed, fallback to Zapper
472+
result = await fetchZapQuote(newQuoteId, newRetryId)
486473
}
487-
if (results[1].status === 'rejected') {
488-
throw results[1].reason
474+
} else {
475+
// Above minimum or not applicable - try both in parallel
476+
const results = await Promise.allSettled([
477+
fetchZapQuote(newQuoteId, newRetryId),
478+
fetchOdosQuote(newQuoteId, newRetryId),
479+
])
480+
481+
const zapResult =
482+
results[0].status === 'fulfilled' ? results[0].value : undefined
483+
const odosResult =
484+
results[1].status === 'fulfilled' ? results[1].value : undefined
485+
486+
const bestQuote = selectBestQuote(zapResult, odosResult)
487+
488+
if (!bestQuote) {
489+
// Both failed, throw the first error
490+
if (results[0].status === 'rejected') {
491+
throw results[0].reason
492+
}
493+
if (results[1].status === 'rejected') {
494+
throw results[1].reason
495+
}
496+
throw new Error('No quotes available')
489497
}
490-
throw new Error('No quotes available')
491-
}
492498

493-
result = bestQuote
499+
result = bestQuote
500+
}
494501
}
495502

496503
const newSourceId = generateSourceId(result.source)
@@ -511,11 +518,12 @@ const useZapSwapQuery = ({
511518
return result
512519
},
513520
enabled:
521+
!disabled &&
514522
(quoteSource === 'best'
515523
? !!zapEndpoint || !!odosEndpoint
516524
: quoteSource === 'zap'
517525
? !!zapEndpoint
518-
: !!odosEndpoint) && !disabled,
526+
: !!odosEndpoint),
519527
refetchInterval: 12000,
520528
retry: 3,
521529
retryDelay: (attempt) => Math.min(1000 * Math.pow(2, attempt), 10000),

0 commit comments

Comments
 (0)