Skip to content
This repository has been archived by the owner on Oct 14, 2022. It is now read-only.

Commit

Permalink
deprecate config
Browse files Browse the repository at this point in the history
  • Loading branch information
solendvega committed Jun 17, 2022
1 parent aa8d96f commit 6bccdd4
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 44 deletions.
31 changes: 29 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable no-loop-func */
import got from 'got';
import { Config } from 'global';
import { Config, Market, MarketBean } from 'global';

export const OBLIGATION_LEN = 1300;
export const RESERVE_LEN = 619;
export const LENDING_MARKET_LEN = 290;
export const ENDPOINTS = [
{
name: 'production',
endpoint: 'https://solana-api.projectserum.com',
endpoint: 'https://solend.rpcpool.com/a3e03ba77d5e870c8c694b19d61c',
},
{
name: 'devnet',
Expand All @@ -25,6 +25,33 @@ function getApp() {
return app;
}

export async function deserializeMarkets(data: MarketBean[]): Promise<MarketBean[]> {
return data;
}

export async function getMarkets(): Promise<MarketBean[]> {
let attemptCount = 0;
let backoffFactor = 1;
const maxAttempt = 10;

do {
try {
if (attemptCount > 0) {
await new Promise((resolve) => setTimeout(resolve, backoffFactor * 10));
backoffFactor *= 2;
}
attemptCount += 1;
const resp = await got(`https://api.solend.fi/v1/markets/configs?deployment=${getApp()}`, { json: true });
const data = resp.body as MarketBean[];
return await deserializeMarkets(data);
} catch (error) {
console.error('error fetching /v1/markets: ', error);
}
} while (attemptCount < maxAttempt);

throw new Error('failed to fetch /v1/markets');
}

export async function getConfig(): Promise<Config> {
let attemptCount = 0;
let backoffFactor = 1;
Expand Down
33 changes: 33 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
export interface MarketBean {
name: string;
isPrimary: boolean;
description: string;
creator: string;
address: string;
authorityAddress: string;
owner: string;
reserves: ReserveBean[];
}

export interface ReserveBean {
liquidityToken: LiquidityTokenBean;
pythOracle: string;
switchboardOracle: string;
address: string;
collateralMintAddress: string;
collateralSupplyAddress: string;
liquidityAddress: string;
liquidityFeeReceiverAddress: string;
userSupplyCap: number;
}

export interface LiquidityTokenBean {
coingeckoID: string;
decimals: number;
logo: string;
mint: string;
name: string;
symbol: string;
volume24h: string;
}

export interface Config {
programID: string;
assets: Asset[];
Expand Down
29 changes: 13 additions & 16 deletions src/libs/actions/liquidateAndRedeem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ import {
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import { getTokenInfo } from 'libs/utils';
import { getTokenInfo, getTokenInfoFromMarket } from 'libs/utils';
import { findWhere, map } from 'underscore';
import { refreshReserveInstruction } from 'models/instructions/refreshReserve';
import { LiquidateObligationAndRedeemReserveCollateral } from 'models/instructions/LiquidateObligationAndRedeemReserveCollateral';
import { refreshObligationInstruction } from 'models/instructions/refreshObligation';
import { Config, Market } from 'global';
import { Config, Market, MarketBean, ReserveBean } from 'global';

export const liquidateAndRedeem = async (
connection: Connection,
config: Config,
payer: Account,
liquidityAmount: number | string,
repayTokenSymbol: string,
withdrawTokenSymbol: string,
lendingMarket: Market,
lendingMarket: MarketBean,
obligation: any,
) => {
const ixs: TransactionInstruction[] = [];
Expand All @@ -31,14 +30,14 @@ export const liquidateAndRedeem = async (
const borrowReserves = map(obligation.info.borrows, (borrow) => borrow.borrowReserve);
const uniqReserveAddresses = [...new Set<String>(map(depositReserves.concat(borrowReserves), (reserve) => reserve.toString()))];
uniqReserveAddresses.forEach((reserveAddress) => {
const reserveInfo = findWhere(lendingMarket!.reserves, {
const reserveInfo: ReserveBean = findWhere(lendingMarket!.reserves, {
address: reserveAddress,
});
const oracleInfo = findWhere(config.oracles.assets, {
asset: reserveInfo!.asset,
});
const oracleInfo = {
priceAddress: reserveInfo.pythOracle,
switchboardFeedAddress: reserveInfo.switchboardOracle
};
const refreshReserveIx = refreshReserveInstruction(
config,
new PublicKey(reserveAddress),
new PublicKey(oracleInfo!.priceAddress),
new PublicKey(oracleInfo!.switchboardFeedAddress),
Expand All @@ -47,14 +46,13 @@ export const liquidateAndRedeem = async (
});

const refreshObligationIx = refreshObligationInstruction(
config,
obligation.pubkey,
depositReserves,
borrowReserves,
);
ixs.push(refreshObligationIx);

const repayTokenInfo = getTokenInfo(config, repayTokenSymbol);
const repayTokenInfo = getTokenInfoFromMarket(lendingMarket, repayTokenSymbol);

// get account that will be repaying the reserve liquidity
const repayAccount = await Token.getAssociatedTokenAddress(
Expand All @@ -64,9 +62,9 @@ export const liquidateAndRedeem = async (
payer.publicKey,
);

const repayReserve = findWhere(lendingMarket.reserves, { asset: repayTokenSymbol });
const withdrawReserve = findWhere(lendingMarket.reserves, { asset: withdrawTokenSymbol });
const withdrawTokenInfo = getTokenInfo(config, withdrawTokenSymbol);
const repayReserve: ReserveBean = findWhere(lendingMarket.reserves, { asset: repayTokenSymbol });
const withdrawReserve: ReserveBean = findWhere(lendingMarket.reserves, { asset: withdrawTokenSymbol });
const withdrawTokenInfo = getTokenInfoFromMarket(lendingMarket, withdrawTokenSymbol);

const rewardedWithdrawalCollateralAccount = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
Expand All @@ -81,7 +79,7 @@ export const liquidateAndRedeem = async (
const createUserCollateralAccountIx = Token.createAssociatedTokenAccountInstruction(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
withdrawReserve.collateralMintAddress,
new PublicKey(withdrawReserve.collateralMintAddress),
rewardedWithdrawalCollateralAccount,
payer.publicKey,
payer.publicKey,
Expand Down Expand Up @@ -112,7 +110,6 @@ export const liquidateAndRedeem = async (

ixs.push(
LiquidateObligationAndRedeemReserveCollateral(
config,
liquidityAmount,
repayAccount,
rewardedWithdrawalCollateralAccount,
Expand Down
57 changes: 50 additions & 7 deletions src/libs/pyth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import SwitchboardProgram from '@switchboard-xyz/sbv2-lite';
import { Connection, PublicKey } from '@solana/web3.js';
import BigNumber from 'bignumber.js';
import { Config, OracleAsset, Reserve } from 'global';
import { Config, MarketBean, OracleAsset, Reserve, ReserveBean } from 'global';

const NULL_ORACLE = 'nu11111111111111111111111111111111111111111';
const SWITCHBOARD_V1_ADDRESS = 'DtmE9D2CSB4L5D6A15mraeEjrGMm6auWVzgaD8hK2tZM';
Expand Down Expand Up @@ -55,10 +55,53 @@ async function getTokenOracleData(
};
}

export async function getTokensOracleData(connection: Connection, config: Config, reserves) {
const promises: any = [];
const oracles = config.oracles.assets;
reserves.forEach((reserve) => { promises.push(getTokenOracleData(connection, config, oracles, reserve)); });
const results = await Promise.all(promises);
return results;
export type TokenOracleData = {
symbol: string;
reserveAddress: string;
mintAddress: string;
decimals: BigNumber;
price: BigNumber;
};

async function getTokenOracleDataMarkets(connection:Connection, reserve:ReserveBean, market:MarketBean) {
let price;
const oracle = {
priceAddress: reserve.pythOracle,
switchboardFeedAddress: reserve.switchboardOracle,
}

if (oracle.priceAddress && oracle.priceAddress !== NULL_ORACLE) {
const pricePublicKey = new PublicKey(oracle.priceAddress);
const result = await connection.getAccountInfo(pricePublicKey);
price = parsePriceData(result!.data).price;
} else {
const pricePublicKey = new PublicKey(oracle.switchboardFeedAddress);
const info = await connection.getAccountInfo(pricePublicKey);
const owner = info?.owner.toString();
if (owner === SWITCHBOARD_V1_ADDRESS) {
const result = AggregatorState.decodeDelimited((info?.data as Buffer)?.slice(1));
price = result?.lastRoundResult?.result;
} else if (owner === SWITCHBOARD_V2_ADDRESS) {
if (!switchboardV2) {
switchboardV2 = await SwitchboardProgram.loadMainnet(connection);
}
const result = switchboardV2.decodeLatestAggregatorValue(info!);
price = result?.toNumber();
} else {
console.error('unrecognized switchboard owner address: ', owner);
}
}

return {
symbol: reserve.liquidityToken.symbol,
reserveAddress: reserve.address,
mintAddress: reserve.liquidityToken.mint,
decimals: new BigNumber(10 ** reserve.liquidityToken.decimals),
price: new BigNumber(price!),
} as TokenOracleData;
}

export async function getTokensOracleData(connection: Connection, market: MarketBean) {
const promises: Promise<any>[] = market.reserves.map((reserve) => getTokenOracleDataMarkets(connection, reserve, market));
return await Promise.all(promises);
}
40 changes: 33 additions & 7 deletions src/libs/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { Connection, PublicKey } from '@solana/web3.js';
import BigNumber from 'bignumber.js';
import { Config, Reserve } from 'global';
import { Config, LiquidityTokenBean, MarketBean, Reserve, ReserveBean } from 'global';
import {
ObligationParser, OBLIGATION_LEN,
} from 'models/layouts/obligation';
Expand Down Expand Up @@ -71,6 +71,21 @@ export function getTokenInfo(config: Config, symbol: string) {
return tokenInfo;
}

export function getTokenInfoFromMarket(market: MarketBean, symbol: string) {
const reserve: ReserveBean = findWhere(market.reserves, { liquidityToken: { symbol } });
const { liquidityToken } = reserve;
if (!reserve) {
throw new Error(`Could not find ${symbol} in config.assets`);
}
return {
name: liquidityToken.name,
symbol: liquidityToken.symbol,
decimals: liquidityToken.decimals,
mintAddress: liquidityToken.mint,
logo: liquidityToken.logo
};
}

export function wait(ms: number) {
return new Promise((res) => setTimeout(res, ms));
}
Expand Down Expand Up @@ -104,14 +119,24 @@ function stripEnd(s: string, c: string) {
return s.slice(0, i + 1);
}

export async function getObligations(connection: Connection, config: Config, lendingMarket) {
const resp = await connection.getProgramAccounts(new PublicKey(config.programID), {
export function getProgramIdForCurrentDeployment() {
if (process.env.APP == "beta") {
return 'BLendhFh4HGnycEDDFhbeFEUYLP4fXB5tTHMoTX8Dch5';
} else if (process.env.APP == "production") {
return 'So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo';
}
return 'So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo';
}

export async function getObligations(connection: Connection, lendingMarketAddr) {
const programID = getProgramIdForCurrentDeployment();
const resp = await connection.getProgramAccounts(new PublicKey(programID), {
commitment: connection.commitment,
filters: [
{
memcmp: {
offset: 10,
bytes: lendingMarket,
bytes: lendingMarketAddr,
},
},
{
Expand All @@ -123,14 +148,15 @@ export async function getObligations(connection: Connection, config: Config, len
return resp.map((account) => ObligationParser(account.pubkey, account.account));
}

export async function getReserves(connection: Connection, config: Config, lendingMarket) {
const resp = await connection.getProgramAccounts(new PublicKey(config.programID), {
export async function getReserves(connection: Connection, lendingMarketAddr) {
const programID = getProgramIdForCurrentDeployment();
const resp = await connection.getProgramAccounts(new PublicKey(programID), {
commitment: connection.commitment,
filters: [
{
memcmp: {
offset: 10,
bytes: lendingMarket,
bytes: lendingMarketAddr,
},
},
{
Expand Down
12 changes: 6 additions & 6 deletions src/liquidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import { getTokensOracleData } from 'libs/pyth';
import { calculateRefreshedObligation } from 'libs/refreshObligation';
import { readSecret } from 'libs/secret';
import { liquidateAndRedeem } from 'libs/actions/liquidateAndRedeem';
import { clusterUrl, getConfig } from './config';
import { clusterUrl, getConfig, getMarkets } from './config';

dotenv.config();

async function runLiquidator() {
const markets = await getMarkets();
const config = await getConfig();
const connection = new Connection(clusterUrl!.endpoint, 'confirmed');

Expand All @@ -32,15 +33,15 @@ async function runLiquidator() {
`);

for (let epoch = 0; ; epoch += 1) {
for (const market of config.markets) {
for (const market of markets) {
// Target specific market if MARKET is specified in docker-compose.yaml
if (process.env.MARKET && process.env.MARKET !== market.address) {
continue;
}

const tokensOracle = await getTokensOracleData(connection, config, market.reserves);
const allObligations = await getObligations(connection, config, market.address);
const allReserves = await getReserves(connection, config, market.address);
const tokensOracle = await getTokensOracleData(connection, market);
const allObligations = await getObligations(connection, market.address);
const allReserves = await getReserves(connection, market.address);

for (let obligation of allObligations) {
try {
Expand Down Expand Up @@ -102,7 +103,6 @@ async function runLiquidator() {
// 50% val of all borrowed assets.
await liquidateAndRedeem(
connection,
config,
payer,
balanceBase,
selectedBorrow.symbol,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import { Config } from 'global';
import * as Layout from 'libs/layout';
import { getProgramIdForCurrentDeployment } from 'libs/utils';
import { LendingInstruction } from './instruction';

/// Repay borrowed liquidity to a reserve to receive collateral at a discount from an unhealthy
Expand All @@ -30,7 +31,6 @@ import { LendingInstruction } from './instruction';
/// 13 `[signer]` User transfer authority ($authority).
/// 14 `[]` Token program id.
export const LiquidateObligationAndRedeemReserveCollateral = (
config: Config,
liquidityAmount: number | BN | string,
sourceLiquidity: PublicKey,
destinationCollateral: PublicKey,
Expand Down Expand Up @@ -85,7 +85,7 @@ export const LiquidateObligationAndRedeemReserveCollateral = (

return new TransactionInstruction({
keys,
programId: new PublicKey(config.programID),
programId: new PublicKey(getProgramIdForCurrentDeployment()),
data,
});
};
Loading

0 comments on commit 6bccdd4

Please sign in to comment.