diff --git a/src/quote/flashmint/nav/index.ts b/src/quote/flashmint/nav/index.ts new file mode 100644 index 00000000..6f29423e --- /dev/null +++ b/src/quote/flashmint/nav/index.ts @@ -0,0 +1 @@ +export * from './provider' diff --git a/src/quote/flashmint/nav/provider.test.ts b/src/quote/flashmint/nav/provider.test.ts new file mode 100644 index 00000000..29de9037 --- /dev/null +++ b/src/quote/flashmint/nav/provider.test.ts @@ -0,0 +1,116 @@ +import { AddressZero } from 'constants/addresses' +import { + IndexZeroExSwapQuoteProvider, + LocalhostProviderUrl, + QuoteTokens, +} from 'tests/utils' +import { wei } from 'utils/numbers' +import { FlashMintNavQuoteRequest, FlashMintNavQuoteProvider } from './provider' +import { Exchange } from 'utils' + +describe('FlashMintNavQuoteProvider()', () => { + const { icusd, usdc, weth } = QuoteTokens + const indexToken = icusd + const chainId = 1 + const provider = LocalhostProviderUrl + const swapQuoteProvider = IndexZeroExSwapQuoteProvider + + test('returns a quote for minting icUSD', async () => { + const inputToken = usdc + const request: FlashMintNavQuoteRequest = { + chainId, + isMinting: true, + inputToken, + outputToken: indexToken, + inputTokenAmount: wei(10, 6), + slippage: 0.5, + } + const quoteProvider = new FlashMintNavQuoteProvider( + provider, + swapQuoteProvider + ) + const quote = await quoteProvider.getQuote(request) + if (!quote) fail() + expect(quote.inputTokenAmount).toEqual(request.inputTokenAmount) + expect(quote.outputTokenAmount.gt(0)).toEqual(true) + expect(quote.reserveAssetSwapData).toEqual({ + exchange: Exchange.None, + fees: [], + path: [], + poolIds: [], + pool: AddressZero, + }) + }) + + test('returns a quote for minting icUSD w/ WETH', async () => { + const inputToken = weth + const request: FlashMintNavQuoteRequest = { + chainId, + isMinting: true, + inputToken, + outputToken: indexToken, + inputTokenAmount: wei(1), + slippage: 0.5, + } + const quoteProvider = new FlashMintNavQuoteProvider( + provider, + swapQuoteProvider + ) + const quote = await quoteProvider.getQuote(request) + if (!quote) fail() + expect(quote.inputTokenAmount).toEqual(request.inputTokenAmount) + expect(quote.outputTokenAmount.gt(0)).toEqual(true) + // TODO: + expect(quote.reserveAssetSwapData).toBeDefined() + }) + + test('returns a quote redeeming icUSD for USDC', async () => { + const outputToken = usdc + const request: FlashMintNavQuoteRequest = { + chainId, + isMinting: false, + inputToken: indexToken, + outputToken, + inputTokenAmount: wei(1), + slippage: 0.5, + } + const quoteProvider = new FlashMintNavQuoteProvider( + provider, + swapQuoteProvider + ) + const quote = await quoteProvider.getQuote(request) + if (!quote) fail() + expect(quote.inputTokenAmount).toEqual(request.inputTokenAmount) + expect(quote.outputTokenAmount.gt(0)).toEqual(true) + expect(quote.reserveAssetSwapData).toEqual({ + exchange: Exchange.None, + fees: [], + path: [], + poolIds: [], + pool: AddressZero, + }) + }) + + // TODO: + test.skip('returns a quote for redeeming icUSD for WETH', async () => { + const outputToken = weth + const request: FlashMintNavQuoteRequest = { + chainId, + isMinting: false, + inputToken: indexToken, + outputToken, + inputTokenAmount: wei(1), + slippage: 0.5, + } + const quoteProvider = new FlashMintNavQuoteProvider( + provider, + swapQuoteProvider + ) + const quote = await quoteProvider.getQuote(request) + if (!quote) fail() + expect(quote.inputTokenAmount).toEqual(request.inputTokenAmount) + expect(quote.outputTokenAmount.gt(0)).toEqual(true) + // TODO: + expect(quote.reserveAssetSwapData).toBeDefined() + }) +}) diff --git a/src/quote/flashmint/nav/provider.ts b/src/quote/flashmint/nav/provider.ts new file mode 100644 index 00000000..aaa1418c --- /dev/null +++ b/src/quote/flashmint/nav/provider.ts @@ -0,0 +1,117 @@ +import { BigNumber } from '@ethersproject/bignumber' + +import { AddressZero } from 'constants/addresses' +import { USDC } from 'constants/tokens' +import { SwapQuoteProvider } from 'quote/swap' +import { + Exchange, + getFlashMintNavContract, + isSameAddress, + slippageAdjustedTokenAmount, + SwapDataV3, +} from 'utils' +import { getRpcProvider } from 'utils/rpc-provider' + +import { QuoteProvider, QuoteToken } from '../../interfaces' + +export interface FlashMintNavQuoteRequest { + chainId: number + isMinting: boolean + inputToken: QuoteToken + outputToken: QuoteToken + // In contrast to other quote providers, we always need the input amount (not the index amount) + inputTokenAmount: BigNumber + slippage: number +} + +export interface FlashMintNavQuoteQuote { + inputTokenAmount: BigNumber + outputTokenAmount: BigNumber + reserveAssetSwapData: SwapDataV3 +} + +export class FlashMintNavQuoteProvider + implements QuoteProvider +{ + constructor( + private readonly rpcUrl: string, + private readonly swapQuoteProvider: SwapQuoteProvider + ) {} + + async getQuote( + request: FlashMintNavQuoteRequest + ): Promise { + const { + chainId, + inputToken, + inputTokenAmount, + isMinting, + outputToken, + slippage, + } = request + + const indexToken = isMinting ? outputToken : inputToken + const usdc = USDC.address! + + const swapQuoteRequest = { + chainId, + inputToken: isMinting ? inputToken.address : usdc, + outputToken: isMinting ? usdc : outputToken.address, + // TODO: + inputAmount: inputTokenAmount.toString(), + // TODO: + // sources: [Exchange.UniV3], + slippage, + } + console.log(swapQuoteRequest) + + let reserveAssetSwapData: SwapDataV3 = { + exchange: Exchange.None, + fees: [], + path: [], + poolIds: [], + pool: AddressZero, + } + if ( + !isSameAddress(swapQuoteRequest.inputToken, swapQuoteRequest.outputToken) + ) { + const res = await this.swapQuoteProvider.getSwapQuote(swapQuoteRequest) + console.log(res) + if (!res?.swapData) return null + reserveAssetSwapData = { + ...res.swapData, + poolIds: [], + } + } + + let estimatedInputOutputAmount: BigNumber = BigNumber.from(0) + const provider = getRpcProvider(this.rpcUrl) + const contract = getFlashMintNavContract(provider) + if (isMinting) { + estimatedInputOutputAmount = await contract.callStatic.getIssueAmount( + indexToken.address, + inputToken.address, + inputTokenAmount, + reserveAssetSwapData + ) + } else { + estimatedInputOutputAmount = await contract.callStatic.getRedeemAmountOut( + indexToken.address, + inputTokenAmount, + outputToken.address, + reserveAssetSwapData + ) + } + const outputTokenAmount = slippageAdjustedTokenAmount( + estimatedInputOutputAmount, + isMinting ? outputToken.decimals : inputToken.decimals, + slippage, + isMinting + ) + return { + inputTokenAmount, + outputTokenAmount, + reserveAssetSwapData, + } + } +}