import { Address } from "@celo/utils/lib/address"; import { balanceOfMulticall, Multicall2 } from "@node-fi/multicall"; import axios from "axios"; import BigNumber from "bignumber.js"; import Web3 from "web3"; import { convertNodeRouteToContractPayload, NodeRoute } from "../Routing"; import { BigNumberString, formatDepositRequest, formatWithdrawRequest, } from "../constants"; import { getOneClickFarm } from "../constants/contracts"; import { applySlippage } from "../utils"; import { Wallet } from "../wallet"; import { DepositData, DepositTransactionDetails, EarnPosition, Reward, YieldFarmDetails, } from "./EarnPosition"; type RouterResponseDeposit = { expectedOut: BigNumberString; routerAddress: Address; underlyingRoutes: NodeRoute[]; depositData: DepositData; oneClickFarmAddress: Address; }; type RouterResponseWithdraw = { directOutput: { [address: Address]: BigNumberString }; routerAddress: Address; underlyingRoutes: NodeRoute[]; }; export class YieldFarm extends EarnPosition { protected rewards: Reward[]; constructor({ rewards, ...base }: YieldFarmDetails, wallet?: Wallet) { super(base, wallet); this.rewards = rewards; } canDirectlySwap(): boolean { return false; } balanceOfMulticall = ( owner: string ): Readonly<{ call: Readonly; parseReturn: (returnData: string) => BigNumber; }> => balanceOfMulticall(this.rewards[this.rewards.length - 1].contract, owner); async getExpectedDeposit( tokenIn: string, amountIn: string ): Promise<{ expectedOut?: BigNumber; routerAddress?: string; routes?: NodeRoute[]; depositData?: DepositData; oneClickFarmAddress?: Address; error?: string; }> { try { const url = formatDepositRequest( tokenIn, amountIn, this.base.depositToken ); const response = await axios.get(url); const { expectedOut, ...rest } = response.data; return { ...rest, expectedOut: new BigNumber(expectedOut) }; } catch (e) { if (axios.isAxiosError(e)) { return { error: e.message }; } return { error: "true" }; } } async withdrawToSelectedToken( amountToWithdraw: BigNumber, tokenOut: string ): Promise<{ directOutput?: { [address: Address]: BigNumber }; expectedOut?: BigNumber | undefined; routerAddress?: string | undefined; underlyingRoutes?: NodeRoute[] | undefined; error?: string | undefined; }> { try { const url = formatWithdrawRequest( this.base.depositToken, amountToWithdraw.toFixed(), tokenOut ); const response = await axios.get(url); const { directOutput, underlyingRoutes, routerAddress } = response.data; const expectedOut = underlyingRoutes.reduce( (accum, cur) => accum.plus(cur.expectedOutputAmount), new BigNumber("0") ); const castedDirectOutput = Object.entries(directOutput).reduce( (accum, [addr, amount]) => ({ ...accum, [addr.toLowerCase()]: new BigNumber(amount), }), {} ); return { expectedOut, directOutput: castedDirectOutput, routerAddress, underlyingRoutes, }; } catch (e) { if (axios.isAxiosError(e)) { return { error: e.message }; } return { error: "true" }; } } async withdrawToUnderlying(amountToWithdraw: BigNumber): Promise<{ directOutput?: { [address: string]: BigNumber } | undefined; error?: string | undefined; }> { try { const url = formatWithdrawRequest( this.base.depositToken, amountToWithdraw.toFixed() ); const response = await axios.get(url); const { directOutput } = response.data; const castedDirectOutput: { [address: Address]: BigNumber } = Object.entries(directOutput).reduce( (accum, [addr, amount]) => ({ ...accum, [addr.toLowerCase()]: new BigNumber(amount), }), {} ); return { directOutput: castedDirectOutput, }; } catch (e) { if (axios.isAxiosError(e)) { return { error: e.message }; } return { error: "true" }; } } public formatTransaction( details: DepositTransactionDetails, to: Address, slippage: number ): { to: string; data: string; gas: string } | undefined { const { depositData, routes, expectedOut, routerAddress, oneClickFarmAddress, } = details; if ( !depositData || !routes || !expectedOut || !routerAddress || !oneClickFarmAddress ) return undefined; const oneClickContract = getOneClickFarm(new Web3(), oneClickFarmAddress); const routeStructs = routes.map( (r) => convertNodeRouteToContractPayload(r, 0.99, depositData.addr) // slippage here is inconsequential as it really matters only at the deposit level ); const minExpected = applySlippage(expectedOut, slippage); const data = oneClickContract.methods .oneClickDeposit( routeStructs, routerAddress, depositData.addr, minExpected.toFixed(0), to, depositData.extra ) .encodeABI(); return { to: oneClickFarmAddress, data, gas: "5000000", }; } public additionalTransactionsForDeposit(_: DepositTransactionDetails): { to: string; data: string; gas: string; }[] { return []; } }