diff --git a/src/interfaces/simulation.ts b/src/interfaces/simulation.ts index b2b01ced..c6269edc 100644 --- a/src/interfaces/simulation.ts +++ b/src/interfaces/simulation.ts @@ -7,7 +7,8 @@ import BigNumber from "bignumber.js"; import { ChainId } from "../chain"; import { ServiceInterface } from "../common"; import { EthAddress, WethAddress, ZeroAddress } from "../helpers"; -import { Address, Integer, SdkError, ZapApprovalTransactionOutput } from "../types"; +import { PickleJars } from "../services/partners/pickle"; +import { Address, Integer, SdkError, ZapApprovalTransactionOutput, ZapProtocol } from "../types"; import { TransactionOutcome } from "../types/custom/simulation"; const baseUrl = "https://simulate.yearn.network"; @@ -20,6 +21,8 @@ const VaultAbi = [ "function pricePerShare() view returns (uint256)" ]; +const PickleJarAbi = ["function token() view returns (address)", "function getRatio() public view returns (uint256)"]; + interface SimulationCallTrace { output: Integer; calls: SimulationCallTrace[]; @@ -82,7 +85,17 @@ export class SimulationInterface extends ServiceInterface slippage?: number ): Promise { const signer = this.ctx.provider.write.getSigner(from); - const vaultContract = new Contract(toVault, VaultAbi, signer); + const zapProtocol = PickleJars.includes(toVault) ? ZapProtocol.PICKLE : ZapProtocol.YEARN; + let abi: string[]; + switch (zapProtocol) { + case ZapProtocol.YEARN: + abi = VaultAbi; + break; + case ZapProtocol.PICKLE: + abi = PickleJarAbi; + break; + } + const vaultContract = new Contract(toVault, abi, signer); const underlyingToken = await vaultContract.token(); const isZapping = underlyingToken !== sellToken; @@ -97,12 +110,18 @@ export class SimulationInterface extends ServiceInterface needsApproving = false; } else { needsApproving = await this.yearn.services.zapper - .zapInApprovalState(from, sellToken) + .zapInApprovalState(from, sellToken, zapProtocol) .then(state => !state.isApproved); } if (needsApproving) { - const approvalTransaction = await this.yearn.services.zapper.zapInApprovalTransaction(from, sellToken, "0"); + const approvalTransaction = await this.yearn.services.zapper.zapInApprovalTransaction( + from, + sellToken, + "0", + zapProtocol + ); + const forkId = await this.createFork(); const approvalSimulationResponse = await this.simulateZapApprovalTransaction(approvalTransaction, forkId); return this.zapIn( @@ -113,13 +132,14 @@ export class SimulationInterface extends ServiceInterface toVault, vaultContract, slippage, + zapProtocol, approvalSimulationResponse.simulation.id, forkId ).finally(async () => { await this.deleteFork(forkId); }); } else { - return this.zapIn(from, sellToken, underlyingToken, amount, toVault, vaultContract, slippage); + return this.zapIn(from, sellToken, underlyingToken, amount, toVault, vaultContract, slippage, zapProtocol); } } else { const needsApproving = await this.depositNeedsApproving(from, sellToken, toVault, amount, signer); @@ -282,11 +302,20 @@ export class SimulationInterface extends ServiceInterface toVault: Address, vaultContract: Contract, slippage: number, + zapProtocol: ZapProtocol, root?: string, forkId?: string ): Promise { const zapToken = sellToken === EthAddress ? ZeroAddress : sellToken; - const zapInParams = await this.yearn.services.zapper.zapIn(from, zapToken, amount, toVault, "0", slippage); + const zapInParams = await this.yearn.services.zapper.zapIn( + from, + zapToken, + amount, + toVault, + "0", + slippage, + zapProtocol + ); const value = new BigNumber(zapInParams.value).toFixed(0); const body = { @@ -305,19 +334,33 @@ export class SimulationInterface extends ServiceInterface const simulationResponse: SimulationResponse = await this.makeSimulationRequest(body, forkId); const assetTokensReceived = new BigNumber(simulationResponse.transaction.transaction_info.call_trace.output); - const pricePerShare = await vaultContract.pricePerShare(); + const pricePerShare = await this.getPricePerShare(vaultContract, zapProtocol); const targetUnderlyingTokensReceived = assetTokensReceived - .times(new BigNumber(pricePerShare.toString())) .div(new BigNumber(10).pow(18)) + .multipliedBy(pricePerShare) .toFixed(0); - const oracleToken = sellToken === EthAddress ? WethAddress : sellToken; - const zapInAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(oracleToken, amount); - const amountReceived = assetTokensReceived.toFixed(0); - const boughtAssetAmountUsdc = await this.yearn.services.oracle.getNormalizedValueUsdc(toVault, amountReceived); - const conversionRate = new BigNumber(boughtAssetAmountUsdc).div(new BigNumber(zapInAmountUsdc)).toNumber(); + let boughtAssetAmountUsdc: BigNumber; + + switch (zapProtocol) { + case ZapProtocol.YEARN: + boughtAssetAmountUsdc = await this.yearn.services.oracle + .getNormalizedValueUsdc(toVault, amountReceived) + .then(price => new BigNumber(price)); + break; + case ZapProtocol.PICKLE: + boughtAssetAmountUsdc = (await this.yearn.services.pickle.getPriceUsd(toVault)) + .dividedBy(new BigNumber(10).pow(18 - 6)) + .multipliedBy(new BigNumber(amountReceived)); + break; + } + + const oracleToken = sellToken === EthAddress ? WethAddress : sellToken; + const zapInAmountUsdc = new BigNumber(await this.yearn.services.oracle.getNormalizedValueUsdc(oracleToken, amount)); + + const conversionRate = boughtAssetAmountUsdc.div(new BigNumber(zapInAmountUsdc)).toNumber(); const result: TransactionOutcome = { sourceTokenAddress: sellToken, @@ -451,6 +494,17 @@ export class SimulationInterface extends ServiceInterface return response; } + private async getPricePerShare(vaultContract: Contract, zapProtocol: ZapProtocol): Promise { + switch (zapProtocol) { + case ZapProtocol.YEARN: + const pps = await vaultContract.pricePerShare(); + return new BigNumber(pps.toString()); + case ZapProtocol.PICKLE: + const ratio = await vaultContract.getRatio(); + return new BigNumber(ratio.toString()); + } + } + /** * Create a new fork that can be used to simulate multiple sequential transactions on * e.g. approval followed by a deposit. diff --git a/src/services/partners/pickle.ts b/src/services/partners/pickle.ts new file mode 100644 index 00000000..886f2dd9 --- /dev/null +++ b/src/services/partners/pickle.ts @@ -0,0 +1,50 @@ +import { getAddress } from "@ethersproject/address"; +import { BigNumber } from "bignumber.js"; + +import { Service } from "../../common"; +import { Address } from "../../types/common"; + +const HourInMilliseconds = 1000 * 60 * 60; +const PickleApiUrl = "https://stkpowy01i.execute-api.us-west-1.amazonaws.com/prod/protocol/pools"; + +export const PickleJars = [ + "0xCeD67a187b923F0E5ebcc77C7f2F7da20099e378" // yvboost-eth +]; + +export class PickleService extends Service { + private pickleJarUSDPrices: Map = new Map(); + private lastFetchedDate: Date = new Date(0); + + /** + * Fetches the USD price of a pickle jar token + * @param jar the address of the jar to fetch + * @returns the price of the jar token in USD + */ + async getPriceUsd(jar: Address): Promise { + const oneHourAgo = new Date(Date.now() - HourInMilliseconds); + if (this.lastFetchedDate < oneHourAgo) { + await this.fetchPickleJarPrices(); + } + return this.pickleJarUSDPrices.get(jar) || new BigNumber(0); + } + + private async fetchPickleJarPrices() { + interface JarDatum { + liquidity_locked: number; + jarAddress: Address; + tokens: number; + } + + const jarData: JarDatum[] = await fetch(PickleApiUrl).then(res => res.json()); + + this.pickleJarUSDPrices.clear(); + this.lastFetchedDate = new Date(); + + const relevantJars = jarData.filter(jar => PickleJars.includes(getAddress(jar.jarAddress))); + + for (const jarDatum of relevantJars) { + const usdPrice = new BigNumber(jarDatum.liquidity_locked / jarDatum.tokens); + this.pickleJarUSDPrices.set(getAddress(jarDatum.jarAddress), usdPrice); + } + } +} diff --git a/src/services/zapper.ts b/src/services/zapper.ts index 8a13515b..357cd509 100644 --- a/src/services/zapper.ts +++ b/src/services/zapper.ts @@ -4,7 +4,13 @@ import { Chains } from "../chain"; import { Service } from "../common"; import { EthAddress, handleHttpError, usdc, ZeroAddress } from "../helpers"; import { Address, Balance, BalancesMap, Integer, Token } from "../types"; -import { GasPrice, ZapApprovalStateOutput, ZapApprovalTransactionOutput, ZapOutput } from "../types/custom/zapper"; +import { + GasPrice, + ZapApprovalStateOutput, + ZapApprovalTransactionOutput, + ZapOutput, + ZapProtocol +} from "../types/custom/zapper"; /** * [[ZapperService]] interacts with the zapper api to gather more insight for @@ -112,9 +118,14 @@ export class ZapperService extends Service { * Fetches the data needed to check token ZapIn contract approval state * @param from - the address that is depositing * @param token - the token to be sold to pay for the deposit + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ - async zapInApprovalState(from: Address, token: Address): Promise { - const url = "https://api.zapper.fi/v1/zap-in/yearn/approval-state"; + async zapInApprovalState( + from: Address, + token: Address, + zapProtocol: ZapProtocol = ZapProtocol.YEARN + ): Promise { + const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/approval-state`; const params = new URLSearchParams({ ownerAddress: from, sellTokenAddress: token, @@ -132,13 +143,15 @@ export class ZapperService extends Service { * @param from - the address that is depositing * @param token - the token to be sold to pay for the deposit * @param gasPrice + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ async zapInApprovalTransaction( from: Address, token: Address, - gasPrice: Integer + gasPrice: Integer, + zapProtocol: ZapProtocol = ZapProtocol.YEARN ): Promise { - const url = "https://api.zapper.fi/v1/zap-in/yearn/approval-transaction"; + const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/approval-transaction`; const params = new URLSearchParams({ gasPrice, ownerAddress: from, @@ -156,9 +169,14 @@ export class ZapperService extends Service { * Fetches the data needed to check token ZapOut contract approval state * @param from - the address that is withdrawing * @param token - the vault token to be withdrawn + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ - async zapOutApprovalState(from: Address, token: Address): Promise { - const url = "https://api.zapper.fi/v1/zap-out/yearn/approval-state"; + async zapOutApprovalState( + from: Address, + token: Address, + zapProtocol: ZapProtocol = ZapProtocol.YEARN + ): Promise { + const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/approval-state`; const params = new URLSearchParams({ ownerAddress: from, sellTokenAddress: token, @@ -176,13 +194,15 @@ export class ZapperService extends Service { * @param from - the address that is withdrawing * @param token - the vault token to be withdrawn * @param gasPrice + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ async zapOutApprovalTransaction( from: Address, token: Address, - gasPrice: Integer + gasPrice: Integer, + zapProtocol: ZapProtocol = ZapProtocol.YEARN ): Promise { - const url = "https://api.zapper.fi/v1/zap-out/yearn/approval-transaction"; + const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/approval-transaction`; const params = new URLSearchParams({ gasPrice, ownerAddress: from, @@ -204,6 +224,7 @@ export class ZapperService extends Service { * @param vault - the vault to zap into * @param gasPrice * @param slippagePercentage - slippage as a decimal + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ async zapIn( from: Address, @@ -211,7 +232,8 @@ export class ZapperService extends Service { amount: Integer, vault: Address, gasPrice: Integer, - slippagePercentage: number + slippagePercentage: number, + zapProtocol: ZapProtocol = ZapProtocol.YEARN ): Promise { let sellToken = token; if (EthAddress === token) { @@ -219,7 +241,7 @@ export class ZapperService extends Service { sellToken = ZeroAddress; } - const url = "https://api.zapper.fi/v1/zap-in/yearn/transaction"; + const url = `https://api.zapper.fi/v1/zap-in/vault/${zapProtocol}/transaction`; const params = new URLSearchParams({ ownerAddress: from, sellTokenAddress: sellToken, @@ -246,6 +268,7 @@ export class ZapperService extends Service { * @param vault - the vault to zap out of * @param gasPrice * @param slippagePercentage - slippage as a decimal + * @param zapProtocol the protocol to use with zapper e.g. Yearn, Pickle */ async zapOut( from: Address, @@ -253,7 +276,8 @@ export class ZapperService extends Service { amount: Integer, vault: Address, gasPrice: Integer, - slippagePercentage: number + slippagePercentage: number, + zapProtocol: ZapProtocol = ZapProtocol.YEARN ): Promise { let toToken = token; if (EthAddress === token) { @@ -261,7 +285,7 @@ export class ZapperService extends Service { toToken = ZeroAddress; } - const url = "https://api.zapper.fi/v1/zap-out/yearn/transaction"; + const url = `https://api.zapper.fi/v1/zap-out/vault/${zapProtocol}/transaction`; const params = new URLSearchParams({ ownerAddress: from, toTokenAddress: toToken, diff --git a/src/types/custom/zapper.ts b/src/types/custom/zapper.ts index edf37c93..faf4fcfe 100644 --- a/src/types/custom/zapper.ts +++ b/src/types/custom/zapper.ts @@ -42,3 +42,8 @@ export interface ZapOutput { gasPrice: Integer; gas: Integer; } + +export enum ZapProtocol { + PICKLE = "pickle", + YEARN = "yearn" +} diff --git a/src/yearn.ts b/src/yearn.ts index 9660598c..95a9ce08 100644 --- a/src/yearn.ts +++ b/src/yearn.ts @@ -10,6 +10,7 @@ import { HelperService } from "./services/helper"; import { IconsService } from "./services/icons"; import { LensService } from "./services/lens"; import { OracleService } from "./services/oracle"; +import { PickleService } from "./services/partners/pickle"; import { SubgraphService } from "./services/subgraph"; import { VisionService } from "./services/vision"; import { ZapperService } from "./services/zapper"; @@ -36,6 +37,8 @@ export class Yearn { vision: VisionService; subgraph: SubgraphService; + pickle: PickleService; + helper: HelperService; }; @@ -74,6 +77,7 @@ export class Yearn { icons: new IconsService(chainId, this.context), vision: new VisionService(chainId, this.context), subgraph: new SubgraphService(chainId, this.context), + pickle: new PickleService(chainId, this.context), helper: new HelperService(chainId, this.context) };