Skip to content

Commit 8eab94c

Browse files
authored
Merge pull request #1653 from IndexCoop/feat/earn
feat: Earn
2 parents e473cd1 + a1c62d2 commit 8eab94c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1176
-1745
lines changed

next.config.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,7 @@ const nextConfig = {
5353
},
5454
{
5555
source: '/',
56-
destination: '/swap',
57-
permanent: true,
58-
},
59-
{
60-
source: '/earn',
61-
destination: '/swap',
56+
destination: '/products',
6257
permanent: true,
6358
},
6459
{

package-lock.json

Lines changed: 17 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@fontsource/open-sauce-sans": "^5.0.7",
2828
"@headlessui/react": "2.1.6",
2929
"@heroicons/react": "^2.1.3",
30-
"@indexcoop/flash-mint-sdk": "3.11.0",
30+
"@indexcoop/flash-mint-sdk": "3.12.0",
3131
"@indexcoop/tokenlists": "3.2.0",
3232
"@rainbow-me/rainbowkit": "^2.0.7",
3333
"@safe-global/api-kit": "2.5.4",

src/app/api/quote/route.ts

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
1-
import {
2-
EthAddress,
3-
FlashMintQuoteProvider,
4-
QuoteToken,
5-
} from '@indexcoop/flash-mint-sdk'
1+
import { EthAddress, QuoteToken } from '@indexcoop/flash-mint-sdk'
62
import {
73
getTokenByChainAndAddress,
84
isAddressEqual,
95
isProductToken,
106
} from '@indexcoop/tokenlists'
11-
import { BigNumber } from 'ethers'
127
import { NextRequest, NextResponse } from 'next/server'
13-
import { Address, isAddress } from 'viem'
14-
15-
import { QuoteType } from '@/lib/hooks/use-best-quote/types'
16-
import { getConfiguredZeroExSwapQuoteProvider } from '@/lib/utils/api/zeroex'
17-
import { getAlchemyBaseUrl } from '@/lib/utils/urls'
8+
import { Address } from 'viem'
189

1910
export interface IndexQuoteRequest {
2011
chainId: number
@@ -39,27 +30,14 @@ export async function POST(req: NextRequest) {
3930
slippage,
4031
} = request
4132

42-
if (slippage < 0 || slippage > 1) {
43-
return BadRequest('Bad Request')
44-
}
45-
46-
if (!isAddress(inputTokenAddress) || !isAddress(outputTokenAddress)) {
47-
return BadRequest('Bad Request')
48-
}
49-
50-
if (!inputAmount && !outputAmount) {
51-
return BadRequest(
52-
'Either `inputAmount` or outputAmount` needs to be set.',
53-
)
54-
}
33+
const inputToken = getQuoteToken(inputTokenAddress, chainId)
34+
const outputToken = getQuoteToken(outputTokenAddress, chainId)
35+
const isMintingIcUsd = outputToken?.quoteToken.symbol === 'icUSD'
5536

56-
if (inputAmount && outputAmount) {
37+
if (!isMintingIcUsd && inputAmount && outputAmount) {
5738
return BadRequest('You can only set `inputAmount` or outputAmount`.')
5839
}
5940

60-
const inputToken = getQuoteToken(inputTokenAddress, chainId)
61-
const outputToken = getQuoteToken(outputTokenAddress, chainId)
62-
6341
if (
6442
!inputToken ||
6543
!outputToken ||
@@ -68,44 +46,48 @@ export async function POST(req: NextRequest) {
6846
return BadRequest('Bad Request')
6947
}
7048

71-
const rpcUrl = getAlchemyBaseUrl(chainId) + process.env.ALCHEMY_API_KEY
72-
const zeroexSwapQuoteProvider =
73-
getConfiguredZeroExSwapQuoteProvider(chainId)
74-
const quoteProvider = new FlashMintQuoteProvider(
75-
rpcUrl,
76-
zeroexSwapQuoteProvider,
77-
)
78-
7949
const isMinting = outputToken.isIndex
80-
const quote = await quoteProvider.getQuote({
81-
isMinting,
82-
inputToken: inputToken.quoteToken,
83-
outputToken: outputToken.quoteToken,
84-
indexTokenAmount: BigNumber.from(
85-
isMinting ? outputAmount! : inputAmount!,
86-
),
87-
slippage,
50+
const quoteRequest: {
51+
account: string
52+
chainId: string
53+
inputToken: string
54+
outputToken: string
55+
inputAmount?: string
56+
outputAmount?: string
57+
slippage: string
58+
} = {
59+
account,
60+
chainId: String(chainId),
61+
inputToken: inputToken.quoteToken.address,
62+
outputToken: outputToken.quoteToken.address,
63+
slippage: String(slippage),
64+
}
65+
if (isMinting) {
66+
quoteRequest.outputAmount = outputAmount
67+
} else {
68+
quoteRequest.inputAmount = inputAmount
69+
}
70+
if (isMintingIcUsd) {
71+
quoteRequest.inputAmount = inputAmount ?? '0'
72+
}
73+
74+
const query = new URLSearchParams(quoteRequest).toString()
75+
const url = `https://api.indexcoop.com/v2/quote?${query}`
76+
const response = await fetch(url, {
77+
method: 'GET',
78+
headers: {
79+
'Content-Type': 'application/json',
80+
'x-api-key': process.env.INDEX_COOP_API_V2_KEY,
81+
} as HeadersInit,
8882
})
8983

84+
const quote = await response.json()
85+
9086
if (!quote) {
9187
return NextResponse.json({ message: 'No quote found.' }, { status: 404 })
9288
}
9389

94-
return NextResponse.json({
95-
type: QuoteType.flashmint,
96-
chainId: quote.chainId,
97-
contract: quote.contract,
98-
contractType: quote.contractType,
99-
takerAddress: account,
100-
isMinting: quote.isMinting,
101-
inputToken: quote.inputToken,
102-
outputToken: quote.outputToken,
103-
indexTokenAmount: quote.indexTokenAmount.toString(),
104-
inputAmount: quote.inputAmount.toString(),
105-
outputAmount: quote.outputAmount.toString(),
106-
slippage: quote.slippage,
107-
transaction: quote.tx,
108-
})
90+
return NextResponse.json(quote)
10991
} catch (error) {
11092
console.error(error)
11193
return NextResponse.json(error, { status: 500 })
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
import { ChevronDownIcon } from '@heroicons/react/20/solid'
22
import Image from 'next/image'
33

4-
import { Token } from '@/constants/tokens'
5-
6-
type BaseTokenSelectorProps = {
7-
baseToken: Token
4+
type TokenSelectorProps = {
5+
selectedToken: { image: string; symbol: string }
86
onClick: () => void
97
}
108

11-
export function BaseTokenSelector({
12-
baseToken,
13-
onClick,
14-
}: BaseTokenSelectorProps) {
15-
const { image, symbol } = baseToken
9+
export function TokenSelector({ selectedToken, onClick }: TokenSelectorProps) {
10+
const { image, symbol } = selectedToken
1611
return (
1712
<div
18-
className='flex cursor-pointer flex-row items-center py-2'
13+
className='flex cursor-pointer flex-row items-center'
1914
onClick={onClick}
2015
>
21-
<Image alt={`${symbol} logo`} src={image} width={24} height={24} />
22-
<span className='text-ic-black dark:text-ic-white mx-1 text-xl font-medium'>
16+
<Image
17+
alt={`${symbol} logo`}
18+
src={image}
19+
width={24}
20+
height={24}
21+
priority
22+
/>
23+
<span className='text-ic-black dark:text-ic-white ml-2 text-base font-bold'>
2324
{symbol}
2425
</span>
25-
<ChevronDownIcon className='dark:text-ic-white text-ic-black size-6' />
26+
<ChevronDownIcon className='dark:text-ic-white text-ic-black size-5' />
2627
</div>
2728
)
2829
}

src/app/earn/components/earn-widget/components/summary.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Disclosure } from '@headlessui/react'
22
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'
33

4-
import { useEarnContext } from '@/app/earn/provider'
54
import { GasFees } from '@/components/gas-fees'
65
import { StyledSkeleton } from '@/components/skeleton'
76

8-
import { useFormattedLeverageData } from '../../../use-formatted-data'
7+
import { useFormattedEarnData } from '../../../use-formatted-data'
98

109
type SummaryQuoteProps = {
1110
label: string
@@ -15,18 +14,19 @@ type SummaryQuoteProps = {
1514

1615
function SummaryQuote(props: SummaryQuoteProps) {
1716
return (
18-
<div className='text-ic-gray-300 flex flex-row items-center justify-between text-xs'>
17+
<div className='text-ic-gray-600 dark:text-ic-gray-300 flex flex-row items-center justify-between text-xs'>
1918
<div className='font-medium'>{props.label}</div>
2019
<div className='flex flex-row gap-1'>
21-
<div className='text-ic-white font-bold'>{props.value}</div>
20+
<div className='text-ic-black dark:text-ic-white font-bold'>
21+
{props.value}
22+
</div>
2223
<div className='font-normal'>{props.valueUsd}</div>
2324
</div>
2425
</div>
2526
)
2627
}
2728

2829
export function Summary() {
29-
const { stats } = useEarnContext()
3030
const {
3131
gasFeesEth,
3232
gasFeesUsd,
@@ -36,14 +36,17 @@ export function Summary() {
3636
ouputAmount,
3737
outputAmountUsd,
3838
shouldShowSummaryDetails,
39-
} = useFormattedLeverageData(stats)
39+
} = useFormattedEarnData()
4040
if (!shouldShowSummaryDetails && !isFetchingQuote) return null
4141
return (
42-
<Disclosure as='div' className='rounded-lg border border-[#3A6060]'>
42+
<Disclosure
43+
as='div'
44+
className='border-ic-gray-100 rounded-lg border dark:border-[#3A6060]'
45+
>
4346
{({ open }) => (
4447
<div className='p-4'>
4548
<dt>
46-
<Disclosure.Button className='text-ic-gray-300 flex w-full items-center justify-between text-left'>
49+
<Disclosure.Button className='text-ic-gray-600 dark:text-ic-gray-300 flex w-full items-center justify-between text-left'>
4750
<span className='text-xs font-medium'>
4851
{open && 'Summary'}
4952
{!open && isFetchingQuote && <StyledSkeleton width={120} />}
@@ -56,7 +59,10 @@ export function Summary() {
5659
{!open && !isFetchingQuote ? (
5760
<GasFees
5861
valueUsd={gasFeesUsd}
59-
styles={{ valueUsdTextColor: 'text-ic-gray-300' }}
62+
styles={{
63+
valueUsdTextColor:
64+
'text-ic-gray-600 dark:text-ic-gray-300',
65+
}}
6066
/>
6167
) : null}
6268
{!open && isFetchingQuote && <StyledSkeleton width={70} />}
@@ -83,7 +89,7 @@ export function Summary() {
8389
value={ouputAmount}
8490
valueUsd={`(${outputAmountUsd})`}
8591
/>
86-
<div className='text-ic-gray-300 flex flex-row items-center justify-between text-xs'>
92+
<div className='text-ic-gray-600 dark:text-ic-gray-300 flex flex-row items-center justify-between text-xs'>
8793
<div className='font-normal'>Network Fee</div>
8894
<div>
8995
<GasFees valueUsd={gasFeesUsd} value={gasFeesEth} />

0 commit comments

Comments
 (0)