diff --git a/src/quote/flashmint/hyeth/component-quotes/across.ts b/src/quote/flashmint/hyeth/component-quotes/across.ts index a3655b6a..df262bf6 100644 --- a/src/quote/flashmint/hyeth/component-quotes/across.ts +++ b/src/quote/flashmint/hyeth/component-quotes/across.ts @@ -4,6 +4,7 @@ import { Contract } from '@ethersproject/contracts' import { WETH } from 'constants/tokens' import { SwapQuoteProvider } from 'quote/swap' +import { isSameAddress } from 'utils/addresses' import { getRpcProvider } from 'utils/rpc-provider' export class AcrossQuoteProvider { @@ -29,6 +30,7 @@ export class AcrossQuoteProvider { acrossLpAmount: bigint, inputToken: string ): Promise { + const outputToken = this.weth const pool = this.getPoolContract() const exchangeRate: BigNumber = await pool.callStatic.exchangeRateCurrent( this.weth @@ -36,10 +38,11 @@ export class AcrossQuoteProvider { const ethAmount = (exchangeRate.toBigInt() * acrossLpAmount) / BigInt(1e18) + this.roundingError + if (isSameAddress(inputToken, outputToken)) return ethAmount const quote = await this.swapQuoteProvider.getSwapQuote({ chainId: 1, inputToken, - outputToken: this.weth, + outputToken, outputAmount: ethAmount.toString(), }) if (!quote) return null @@ -50,6 +53,7 @@ export class AcrossQuoteProvider { acrossLpAmount: bigint, outputToken: string ): Promise { + const inputToken = this.weth const pool = this.getPoolContract() const exchangeRate: BigNumber = await pool.callStatic.exchangeRateCurrent( this.weth @@ -57,9 +61,10 @@ export class AcrossQuoteProvider { const ethAmount = (exchangeRate.toBigInt() * acrossLpAmount) / BigInt(1e18) + this.roundingError + if (isSameAddress(inputToken, outputToken)) return ethAmount const quote = await this.swapQuoteProvider.getSwapQuote({ chainId: 1, - inputToken: this.weth, + inputToken, outputToken, inputAmount: ethAmount.toString(), }) diff --git a/src/quote/flashmint/hyeth/component-quotes/index.ts b/src/quote/flashmint/hyeth/component-quotes/index.ts new file mode 100644 index 00000000..761eb472 --- /dev/null +++ b/src/quote/flashmint/hyeth/component-quotes/index.ts @@ -0,0 +1,162 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { Address, isAddressEqual } from 'viem' + +import { SwapQuoteProvider } from 'quote/swap' + +import { QuoteToken } from '../../../interfaces' + +import { AcrossQuoteProvider } from './across' +import { InstadappQuoteProvider } from './instadapp' +import { PendleQuoteProvider } from './pendle' + +interface ComponentQuotesResult { + componentQuotes: string[] + inputOutputTokenAmount: bigint +} + +export class ComponentQuotesProvider { + constructor( + readonly chainId: number, + readonly slippage: number, + readonly wethAddress: string, + readonly rpcUrl: string, + readonly swapQuoteProvider: SwapQuoteProvider + ) {} + + isAcross(token: string) { + return isAddressEqual( + token as Address, + '0x28F77208728B0A45cAb24c4868334581Fe86F95B' + ) + } + + isInstdapp(token: string) { + return isAddressEqual( + token as Address, + '0xA0D3707c569ff8C87FA923d3823eC5D81c98Be78' + ) + } + + isPendle(token: string) { + const pendleTokens: Address[] = [ + '0x1c085195437738d73d75DC64bC5A3E098b7f93b1', + '0x6ee2b5E19ECBa773a352E5B21415Dc419A700d1d', + '0xf7906F274c174A52d444175729E3fa98f9bde285', + ] + return pendleTokens.some((pendleToken) => + isAddressEqual(pendleToken, token as Address) + ) + } + + async getComponentQuotes( + components: string[], + positions: BigNumber[], + isMinting: boolean, + inputToken: QuoteToken, + outputToken: QuoteToken + ): Promise { + if (components.length === 0 || positions.length === 0) return null + if (components.length !== positions.length) return null + + const { swapQuoteProvider } = this + + const inputTokenAddress = this.getTokenAddressOrWeth(inputToken) + const outputTokenAddress = this.getTokenAddressOrWeth(outputToken) + + const quotePromises: Promise[] = [] + + for (let i = 0; i < components.length; i += 1) { + const index = i + const component = components[index] + const amount = positions[index].toBigInt() + + if (this.isAcross(component)) { + const acrossQuoteProvider = new AcrossQuoteProvider( + this.rpcUrl, + swapQuoteProvider + ) + if (isMinting) { + const quotePromise = acrossQuoteProvider.getDepositQuote( + amount, + inputTokenAddress + ) + quotePromises.push(quotePromise) + } else { + const quotePromise = acrossQuoteProvider.getWithdrawQuote( + amount, + outputTokenAddress + ) + quotePromises.push(quotePromise) + } + } + + if (this.isInstdapp(component)) { + const instadappProvider = new InstadappQuoteProvider( + this.rpcUrl, + swapQuoteProvider + ) + if (isMinting) { + const quotePromise = instadappProvider.getMintQuote( + component, + amount, + inputTokenAddress + ) + quotePromises.push(quotePromise) + } else { + const quotePromise = instadappProvider.getRedeemQuote( + component, + amount, + outputTokenAddress + ) + quotePromises.push(quotePromise) + } + } + + if (this.isPendle(component)) { + const pendleQuoteProvider = new PendleQuoteProvider( + this.rpcUrl, + swapQuoteProvider + ) + if (isMinting) { + const quotePromise = pendleQuoteProvider.getDepositQuote( + component, + amount, + inputTokenAddress + ) + quotePromises.push(quotePromise) + } else { + const quotePromise = pendleQuoteProvider.getWithdrawQuote( + component, + amount, + outputTokenAddress + ) + quotePromises.push(quotePromise) + } + } + } + const resultsWithNull = await Promise.all(quotePromises) + const results: bigint[] = resultsWithNull.filter( + (e): e is Exclude => e !== null + ) + if (results.length !== resultsWithNull.length) return null + // const componentQuotes = results.map((result) => result.callData) + const inputOutputTokenAmount = results + .map((result) => result) + .reduce((prevValue, currValue) => { + return currValue + prevValue + }) + return { + componentQuotes: [], + inputOutputTokenAmount, + } + } + + /** + * Returns the WETH address if token is ETH. Otherwise the token's address. + * @param token A token of type QuoteToken. + * @returns a token address as string + */ + getTokenAddressOrWeth(token: QuoteToken): string { + return token.symbol === 'ETH' ? this.wethAddress : token.address + } +} diff --git a/src/quote/flashmint/hyeth/component-quotes/pendle.ts b/src/quote/flashmint/hyeth/component-quotes/pendle.ts index 9ac4064a..1e6dc30f 100644 --- a/src/quote/flashmint/hyeth/component-quotes/pendle.ts +++ b/src/quote/flashmint/hyeth/component-quotes/pendle.ts @@ -6,6 +6,7 @@ import FLASHMINT_HYETH_ABI from 'constants/abis/FlashMintHyEth.json' import { FlashMintHyEthAddress } from 'constants/contracts' import { WETH } from 'constants/tokens' import { SwapQuoteProvider } from 'quote/swap' +import { isSameAddress } from 'utils/addresses' import { getRpcProvider } from 'utils/rpc-provider' export class PendleQuoteProvider { @@ -49,6 +50,7 @@ export class PendleQuoteProvider { position: bigint, inputToken: string ): Promise { + const outputToken = this.weth const fmHyEth = this.getFlashMintHyEth() const market = await fmHyEth.pendleMarkets(component) // const ptContract = this.getPtContract(component) @@ -57,10 +59,11 @@ export class PendleQuoteProvider { const routerContract = this.getRouterStatic(this.routerStaticMainnet) const assetRate: BigNumber = await routerContract.getPtToAssetRate(market) const ethAmount = (position * assetRate.toBigInt()) / BigInt(1e18) + if (isSameAddress(inputToken, outputToken)) return ethAmount const quote = await this.swapQuoteProvider.getSwapQuote({ chainId: 1, inputToken, - outputToken: this.weth, + outputToken, outputAmount: ethAmount.toString(), }) if (!quote) return null @@ -72,14 +75,16 @@ export class PendleQuoteProvider { position: bigint, outputToken: string ): Promise { + const inputToken = this.weth const fmHyEth = this.getFlashMintHyEth() const market = await fmHyEth.pendleMarkets(component) const routerContract = this.getRouterStatic(this.routerStaticMainnet) const assetRate: BigNumber = await routerContract.getPtToAssetRate(market) const ethAmount = (position * assetRate.toBigInt()) / BigInt(1e18) + if (isSameAddress(inputToken, outputToken)) return ethAmount const quote = await this.swapQuoteProvider.getSwapQuote({ chainId: 1, - inputToken: this.weth, + inputToken, outputToken, inputAmount: ethAmount.toString(), }) diff --git a/src/quote/flashmint/hyeth/provider.test.ts b/src/quote/flashmint/hyeth/provider.test.ts index 36b64446..ad362cd7 100644 --- a/src/quote/flashmint/hyeth/provider.test.ts +++ b/src/quote/flashmint/hyeth/provider.test.ts @@ -14,7 +14,7 @@ import { import { FlashMintHyEthQuoteProvider } from './provider' const rpcUrl = LocalhostProviderUrl -// const swapQuoteProvider = IndexZeroExSwapQuoteProvider +const swapQuoteProvider = IndexZeroExSwapQuoteProvider const { eth, hyeth, usdc, weth } = QuoteTokens const indexToken = hyeth @@ -32,7 +32,10 @@ describe('FlashMintHyEthQuoteProvider()', () => { indexTokenAmount: wei(1).toBigInt(), slippage: 0.5, } - const quoteProvider = new FlashMintHyEthQuoteProvider(rpcUrl) + const quoteProvider = new FlashMintHyEthQuoteProvider( + rpcUrl, + swapQuoteProvider + ) const quote = await quoteProvider.getQuote(request) if (!quote) fail() expect(quote.indexTokenAmount).toEqual(request.indexTokenAmount) @@ -67,7 +70,10 @@ describe('FlashMintHyEthQuoteProvider()', () => { indexTokenAmount: wei(1).toBigInt(), slippage: 0.5, } - const quoteProvider = new FlashMintHyEthQuoteProvider(rpcUrl) + const quoteProvider = new FlashMintHyEthQuoteProvider( + rpcUrl, + swapQuoteProvider + ) const quote = await quoteProvider.getQuote(request) if (!quote) fail() expect(quote.indexTokenAmount).toEqual(request.indexTokenAmount) @@ -116,7 +122,10 @@ describe('FlashMintHyEthQuoteProvider()', () => { indexTokenAmount: wei(1).toBigInt(), slippage: 0.5, } - const quoteProvider = new FlashMintHyEthQuoteProvider(rpcUrl) + const quoteProvider = new FlashMintHyEthQuoteProvider( + rpcUrl, + swapQuoteProvider + ) const quote = await quoteProvider.getQuote(request) if (!quote) fail() expect(quote.indexTokenAmount).toEqual(request.indexTokenAmount) @@ -165,7 +174,10 @@ describe('FlashMintHyEthQuoteProvider()', () => { indexTokenAmount: wei(1).toBigInt(), slippage: 0.5, } - const quoteProvider = new FlashMintHyEthQuoteProvider(rpcUrl) + const quoteProvider = new FlashMintHyEthQuoteProvider( + rpcUrl, + swapQuoteProvider + ) const quote = await quoteProvider.getQuote(request) if (!quote) fail() expect(quote.indexTokenAmount).toEqual(request.indexTokenAmount) @@ -200,7 +212,10 @@ describe('FlashMintHyEthQuoteProvider()', () => { indexTokenAmount: wei(1).toBigInt(), slippage: 0.5, } - const quoteProvider = new FlashMintHyEthQuoteProvider(rpcUrl) + const quoteProvider = new FlashMintHyEthQuoteProvider( + rpcUrl, + swapQuoteProvider + ) const quote = await quoteProvider.getQuote(request) if (!quote) fail() expect(quote.indexTokenAmount).toEqual(request.indexTokenAmount) diff --git a/src/quote/flashmint/hyeth/provider.ts b/src/quote/flashmint/hyeth/provider.ts index f692a19e..48111aa4 100644 --- a/src/quote/flashmint/hyeth/provider.ts +++ b/src/quote/flashmint/hyeth/provider.ts @@ -1,17 +1,15 @@ -import { Contract } from '@ethersproject/contracts' - -import FLASHMINT_HYETH_ABI from 'constants/abis/FlashMintHyEth.json' -import { FlashMintHyEthAddress } from 'constants/contracts' +import { WETH } from 'constants/tokens' import { QuoteProvider, QuoteToken } from 'quote/interfaces' -import { getRpcProvider } from 'utils/rpc-provider' -import { SwapData, wei } from 'utils' +import { SwapQuoteProvider } from 'quote/swap' +import { SwapData } from 'utils' +import { ComponentQuotesProvider } from './component-quotes' +import { getRequiredComponents } from './issuance' import { getComponentsSwapData, getEthToInputOutputTokenSwapData, getInputTokenToEthSwapData, } from './swap-data' -import { BigNumber } from '@ethersproject/bignumber' export interface FlashMintHyEthQuoteRequest { isMinting: boolean @@ -37,13 +35,16 @@ export interface FlashMintHyEthQuote { export class FlashMintHyEthQuoteProvider implements QuoteProvider { - constructor(private readonly rpcUrl: string) {} + constructor( + private readonly rpcUrl: string, + private readonly swapQuoteProvider: SwapQuoteProvider + ) {} async getQuote( request: FlashMintHyEthQuoteRequest ): Promise { - const provider = getRpcProvider(this.rpcUrl) - const { indexTokenAmount, inputToken, isMinting, outputToken } = request + const { indexTokenAmount, inputToken, isMinting, outputToken, slippage } = + request const componentsSwapData = getComponentsSwapData(isMinting) // Only relevant for minting ERC-20's const swapDataInputTokenToEth = isMinting @@ -52,27 +53,36 @@ export class FlashMintHyEthQuoteProvider const inputOutputToken = isMinting ? inputToken : outputToken const swapDataEthToInputOutputToken = getEthToInputOutputTokenSwapData(inputOutputToken) - // TODO: static call write functions? - const indexToken = isMinting ? outputToken : inputToken - const contract = new Contract( - FlashMintHyEthAddress, - FLASHMINT_HYETH_ABI, - provider + + const { components, positions } = await getRequiredComponents( + request, + this.rpcUrl + ) + + // Mainnet only for now + const chainId = 1 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const wethAddress = WETH.address! + const quoteProvider = new ComponentQuotesProvider( + chainId, + slippage, + wethAddress, + this.rpcUrl, + this.swapQuoteProvider + ) + const quoteResult = await quoteProvider.getComponentQuotes( + components, + positions, + isMinting, + inputToken, + outputToken ) - // TODO: switch to provider.call(tx) from builder to handle issue/redeem - // TODO: just for testing, delete later - const inputOutputTokenAmount: BigNumber = - await contract.callStatic.issueExactSetFromETH( - indexToken.address, - indexTokenAmount, - componentsSwapData, - // TODO: - { value: wei(1) } - ) - console.log(inputOutputTokenAmount.toString()) + if (!quoteResult) return null + + const inputOutputTokenAmount = quoteResult.inputOutputTokenAmount return { indexTokenAmount, - inputOutputTokenAmount: inputOutputTokenAmount.toBigInt(), + inputOutputTokenAmount, componentsSwapData, swapDataInputTokenToEth, swapDataEthToInputOutputToken, diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts new file mode 100644 index 00000000..79e92611 --- /dev/null +++ b/src/utils/addresses.ts @@ -0,0 +1,5 @@ +import { Address, isAddressEqual } from 'viem' + +export function isSameAddress(address1: string, address2: string): boolean { + return isAddressEqual(address1 as Address, address2 as Address) +}