Skip to content

Commit

Permalink
Merge pull request #32 from curvefi/feat/enable-fetch-by-api
Browse files Browse the repository at this point in the history
feat: add fetch by api
  • Loading branch information
fedorovdg authored Sep 26, 2024
2 parents 1d126ca + 024e722 commit 5681eb2
Show file tree
Hide file tree
Showing 6 changed files with 591 additions and 154 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@curvefi/lending-api",
"version": "2.2.1",
"version": "2.3.0",
"description": "JavaScript library for Curve Lending",
"main": "lib/index.js",
"author": "Macket",
Expand Down
29 changes: 28 additions & 1 deletion src/external-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import axios from "axios";
import memoize from "memoizee";
import BigNumber from 'bignumber.js';
import { lending } from "./lending.js";
import { IExtendedPoolDataFromApi, INetworkName, IPoolFactory, I1inchSwapData, IDict } from "./interfaces";
import {
IExtendedPoolDataFromApi,
INetworkName,
IPoolFactory,
I1inchSwapData,
IDict,
IMarketData,
} from "./interfaces";


const _getPoolsFromApi = memoize(
Expand Down Expand Up @@ -197,3 +204,23 @@ export const _getSpotPrice1inch = memoize(
maxAge: 10 * 1000, // 10s
}
)

export const _getMarketsData = memoize(
async (network: INetworkName): Promise<IMarketData> => {
const url = `https://api.curve.fi/api/getLendingVaults/${network}/oneway`;
const response = await axios.get(
url,
{
headers: {"accept": "application/json"},
validateStatus: () => true,
});
if (response.status !== 200) {
throw Error(`Fetch error: ${response.status} ${response.statusText}`);
}
return response.data.data;
},
{
promise: true,
maxAge: 10 * 1000, // 10s
}
)
68 changes: 68 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,71 @@ export interface I1inchSwapData {
protocols: I1inchRoute[],
slippage: number,
}

interface Rates {
borrowApr: number;
borrowApy: number;
borrowApyPcent: number;
lendApr: number;
lendApy: number;
lendApyPcent: number;
}

interface Assets {
borrowed: AssetDetail;
collateral: AssetDetail;
}

interface AssetDetail {
symbol: string;
decimals: number;
address: string;
blockchainId: string;
usdPrice: number;
}

interface VaultShares {
pricePerShare: number;
totalShares: number;
}

interface Total {
total: number;
usdTotal: number;
}

interface LendingVaultUrls {
deposit: string;
withdraw: string;
}

interface AmmBalances {
ammBalanceBorrowed: number;
ammBalanceBorrowedUsd: number;
ammBalanceCollateral: number;
ammBalanceCollateralUsd: number;
}

export interface IMarketDataAPI {
id: string;
name: string;
address: string;
controllerAddress: string;
ammAddress: string;
monetaryPolicyAddress: string;
rates: Rates;
gaugeAddress: string;
gaugeRewards: any[];
assets: Assets;
vaultShares: VaultShares;
totalSupplied: Total;
borrowed: Total;
availableToBorrow: Total;
lendingVaultUrls: LendingVaultUrls;
usdTotal: number;
ammBalances: AmmBalances;
}

export interface IMarketData {
lendingVaultData: IMarketDataAPI[]
}
205 changes: 162 additions & 43 deletions src/lending.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { ethers,
Contract, Networkish, BigNumberish, Numeric, AbstractProvider } from "ethers";
import { Provider as MulticallProvider, Contract as MulticallContract, Call } from 'ethcall';
import { IChainId, ILending, IDict, INetworkName, ICurveContract, IOneWayMarket, ICoin } from "./interfaces.js";
import {
IChainId,
ILending,
IDict,
INetworkName,
ICurveContract,
IOneWayMarket,
ICoin,
IMarketDataAPI,
} from "./interfaces.js";
import OneWayLendingFactoryABI from "./constants/abis/OneWayLendingFactoryABI.json" assert { type: 'json' };
import ERC20ABI from './constants/abis/ERC20.json' assert { type: 'json' };
import LlammaABI from './constants/abis/Llamma.json' assert { type: 'json' };
Expand Down Expand Up @@ -54,6 +63,7 @@ import {
import {L2Networks} from "./constants/L2Networks";
import { createCall, handleMultiCallResponse} from "./utils.js";
import {cacheKey, cacheStats} from "./cache/index.js";
import {_getMarketsData} from "./external-api.js";

export const NETWORK_CONSTANTS: { [index: number]: any } = {
1: {
Expand Down Expand Up @@ -376,68 +386,131 @@ class Lending implements ILending {
return handleMultiCallResponse(callsMap, res)
}

getCoins = async (collateral_tokens: string[], borrowed_tokens: string[]): Promise<IDict<ICoin>> => {
const calls: Call[] = [];
const coins = new Set([...collateral_tokens, ...borrowed_tokens])
const callsMap = ['name', 'decimals', 'symbol']

coins.forEach((coin:string) => {
this.setContract(coin, ERC20ABI);
callsMap.forEach((item) => {
calls.push(createCall(this.contracts[coin],item, []))
})
})
getFactoryMarketDataByAPI = async () => {
const apiData = (await _getMarketsData(this.constants.NETWORK_NAME)).lendingVaultData;

const result: Record<string, string[]> = {
names: [],
amms: [],
controllers: [],
borrowed_tokens: [],
collateral_tokens: [],
monetary_policies: [],
vaults: [],
gauges: [],
};

const res = await this.multicallProvider.all(calls);
apiData.forEach((market: IMarketDataAPI) => {
result.names.push(market.name);
result.amms.push(market.ammAddress.toLowerCase());
result.controllers.push(market.controllerAddress.toLowerCase());
result.borrowed_tokens.push(market.assets.borrowed.address.toLowerCase());
result.collateral_tokens.push(market.assets.collateral.address.toLowerCase());
result.monetary_policies.push(market.monetaryPolicyAddress.toLowerCase());
result.vaults.push(market.address.toLowerCase());
result.gauges.push(market.gaugeAddress?.toLowerCase() || this.constants.ZERO_ADDRESS);
});

return result;
}

const {name, decimals, symbol} = handleMultiCallResponse(callsMap, res)
const COINS_DATA: IDict<ICoin> = {}
getCoins = async (collateral_tokens: string[], borrowed_tokens: string[], useApi = false): Promise<IDict<ICoin>> => {
const coins = new Set([...collateral_tokens, ...borrowed_tokens]);
const COINS_DATA: IDict<ICoin> = {};

if (useApi) {
const apiData = (await _getMarketsData(this.constants.NETWORK_NAME)).lendingVaultData;
apiData.forEach((market) => {
const borrowedCoin = market.assets.borrowed;
const collateralCoin = market.assets.collateral;

if (coins.has(borrowedCoin.address)) {
COINS_DATA[borrowedCoin.address] = {
address: borrowedCoin.address,
decimals: borrowedCoin.decimals,
name: borrowedCoin.symbol,
symbol: borrowedCoin.symbol,
};
}

Array.from(coins).forEach((coin: string, index: number) => {
COINS_DATA[coin] = {
address: coin,
decimals: Number(decimals[index]),
name: name[index],
symbol: symbol[index],
}
})
if (coins.has(collateralCoin.address)) {
COINS_DATA[collateralCoin.address] = {
address: collateralCoin.address,
decimals: collateralCoin.decimals,
name: collateralCoin.symbol,
symbol: collateralCoin.symbol,
};
}
});
} else {
const calls: Call[] = [];
const callsMap = ['name', 'decimals', 'symbol'];

coins.forEach((coin: string) => {
this.setContract(coin, ERC20ABI);
callsMap.forEach((item) => {
calls.push(createCall(this.contracts[coin], item, []));
});
});

const res = await this.multicallProvider.all(calls);
const { name, decimals, symbol } = handleMultiCallResponse(callsMap, res);

Array.from(coins).forEach((coin: string, index: number) => {
COINS_DATA[coin] = {
address: coin,
decimals: Number(decimals[index]),
name: name[index],
symbol: symbol[index],
};
});
}

return COINS_DATA;

}

fetchStats = async (amms: string[], controllers: string[], vaults: string[], borrowed_tokens: string[], collateral_tokens: string[]) => {
cacheStats.clear();

const calls: Call[] = [];
const marketCount = controllers.length;

const calls: Call[] = [];

for (let i = 0; i < marketCount; i++) {
calls.push(createCall(this.contracts[controllers[i]],'total_debt', []))
calls.push(createCall(this.contracts[vaults[i]],'totalAssets', [controllers[i]]))
calls.push(createCall(this.contracts[borrowed_tokens[i]],'balanceOf', [controllers[i]]))
calls.push(createCall(this.contracts[amms[i]],'rate', []))
calls.push(createCall(this.contracts[borrowed_tokens[i]],'balanceOf', [amms[i]]))
calls.push(createCall(this.contracts[amms[i]],'admin_fees_x', []))
calls.push(createCall(this.contracts[amms[i]],'admin_fees_y', []))
calls.push(createCall(this.contracts[collateral_tokens[i]],'balanceOf', [amms[i]]))
calls.push(createCall(this.contracts[controllers[i]], 'total_debt', []));
calls.push(createCall(this.contracts[vaults[i]], 'totalAssets', [controllers[i]]));
calls.push(createCall(this.contracts[borrowed_tokens[i]], 'balanceOf', [controllers[i]]));
calls.push(createCall(this.contracts[amms[i]], 'rate', []));
calls.push(createCall(this.contracts[borrowed_tokens[i]], 'balanceOf', [amms[i]]));
calls.push(createCall(this.contracts[amms[i]], 'admin_fees_x', []));
calls.push(createCall(this.contracts[amms[i]], 'admin_fees_y', []));
calls.push(createCall(this.contracts[collateral_tokens[i]], 'balanceOf', [amms[i]]));
}

const res = await this.multicallProvider.all(calls);

for (let i = 0; i < marketCount; i++) {
cacheStats.set(cacheKey(controllers[i], 'total_debt'), res[(i*8) + 0]);
cacheStats.set(cacheKey(vaults[i], 'totalAssets', controllers[i]), res[(i*8) + 1]);
cacheStats.set(cacheKey(borrowed_tokens[i], 'balanceOf', controllers[i]), res[(i*8) + 2]);
cacheStats.set(cacheKey(amms[i], 'rate'), res[(i*8) + 3]);
cacheStats.set(cacheKey(borrowed_tokens[i], 'balanceOf', amms[i]), res[(i*8) + 4]);
cacheStats.set(cacheKey(amms[i], 'admin_fees_x'), res[(i*8) + 5]);
cacheStats.set(cacheKey(amms[i], 'admin_fees_y'), res[(i*8) + 6]);
cacheStats.set(cacheKey(collateral_tokens[i], 'balanceOf', amms[i]), res[(i*8) + 7]);
cacheStats.set(cacheKey(controllers[i], 'total_debt'), res[(i * 8) + 0]);
cacheStats.set(cacheKey(vaults[i], 'totalAssets', controllers[i]), res[(i * 8) + 1]);
cacheStats.set(cacheKey(borrowed_tokens[i], 'balanceOf', controllers[i]), res[(i * 8) + 2]);
cacheStats.set(cacheKey(amms[i], 'rate'), res[(i * 8) + 3]);
cacheStats.set(cacheKey(borrowed_tokens[i], 'balanceOf', amms[i]), res[(i * 8) + 4]);
cacheStats.set(cacheKey(amms[i], 'admin_fees_x'), res[(i * 8) + 5]);
cacheStats.set(cacheKey(amms[i], 'admin_fees_y'), res[(i * 8) + 6]);
cacheStats.set(cacheKey(collateral_tokens[i], 'balanceOf', amms[i]), res[(i * 8) + 7]);
}
};


fetchOneWayMarkets = async (useAPI = true) => {
if(useAPI) {
await this._fetchOneWayMarketsByAPI()
} else {
await this._fetchOneWayMarketsByBlockchain()
}
}

fetchOneWayMarkets = async () => {
_fetchOneWayMarketsByBlockchain = async () => {
const {names, amms, controllers, borrowed_tokens, collateral_tokens, monetary_policies, vaults, gauges} = await this.getFactoryMarketData()
const COIN_DATA = await this.getCoins(collateral_tokens, borrowed_tokens);
for (const c in COIN_DATA) {
Expand Down Expand Up @@ -483,6 +556,52 @@ class Lending implements ILending {
await this.fetchStats(amms, controllers, vaults, borrowed_tokens, collateral_tokens);
}

_fetchOneWayMarketsByAPI = async () => {
const {names, amms, controllers, borrowed_tokens, collateral_tokens, monetary_policies, vaults, gauges} = await this.getFactoryMarketDataByAPI()
const COIN_DATA = await this.getCoins(collateral_tokens, borrowed_tokens, true);
for (const c in COIN_DATA) {
this.constants.DECIMALS[c] = COIN_DATA[c].decimals;
}

amms.forEach((amm: string, index: number) => {
this.setContract(amms[index], LlammaABI);
this.setContract(controllers[index], ControllerABI);
this.setContract(monetary_policies[index], MonetaryPolicyABI);
this.setContract(vaults[index], VaultABI);
if(gauges[index]){
this.setContract(gauges[index], this.chainId === 1 ? GaugeABI : SidechainGaugeABI);
}
COIN_DATA[vaults[index]] = {
address: vaults[index],
decimals: 18,
name: "Curve Vault for " + COIN_DATA[borrowed_tokens[index]].name,
symbol: "cv" + COIN_DATA[borrowed_tokens[index]].symbol,
};
COIN_DATA[gauges[index]] = {
address: gauges[index],
decimals: 18,
name: "Curve.fi " + COIN_DATA[borrowed_tokens[index]].name + " Gauge Deposit",
symbol: "cv" + COIN_DATA[borrowed_tokens[index]].symbol + "-gauge",
};
this.constants.DECIMALS[vaults[index]] = 18;
this.constants.DECIMALS[gauges[index]] = 18;
this.constants.ONE_WAY_MARKETS[`one-way-market-${index}`] = {
name: names[index],
addresses: {
amm: amms[index],
controller: controllers[index],
borrowed_token: borrowed_tokens[index],
collateral_token: collateral_tokens[index],
monetary_policy: monetary_policies[index],
vault: vaults[index],
gauge: gauges[index],
},
borrowed_token: COIN_DATA[borrowed_tokens[index]],
collateral_token: COIN_DATA[collateral_tokens[index]],
}
})
}

formatUnits(value: BigNumberish, unit?: string | Numeric): string {
return ethers.formatUnits(value, unit);
}
Expand Down Expand Up @@ -513,4 +632,4 @@ class Lending implements ILending {
}
}

export const lending = new Lending();
export const lending = new Lending();
Loading

0 comments on commit 5681eb2

Please sign in to comment.